Git Product home page Git Product logo

Comments (18)

EnricoMassone avatar EnricoMassone commented on June 12, 2024 1

Based on my understanding the plan here is to make functionally equivalent the usage of the named caches feature of Fusion Cache with the keyed services feature of Microsoft DI.

Based on your explanation, you want that any code using a named cache registration should also be able to resolve the named cache instance by using the [FromKeyedServices attribute]. This basically means, as you said, to also register a keyed service each time a named cache is registered (unfortunately, doing that implicitly has all the problems you very well explained).

I have a few questions, to better understand your idea and the reasons behind it. Here are my questions:

  1. why do you want to do that ? Why not simply waiting to see the final decision made for the IHttpClientFactory and copy the same approach in Fusion Cache ?
  2. do you also want to support the reverse relationship ? I mean, being able to resolve a named cache from the IFusionCacheProvider service if a keyed service with the same name is registered on the service provider. Or, instead, you just want to make the named cache feature (which is a custom solution in Fusion Cache) to work well with the keyed service feature (which is the built-in solution proposed by Microsoft for basically the same problem) ?
  3. what is your long term plan ? Do you want to maintain both features and make them working together, or instead your long term plan is to deprecate the named cache feature and promote the usage of keyed services instead ?

Maybe answering these questions could help in deciding whether or not to implement this feature.

By the way, I think that a very big advantage of the proposed opt-in approach is that by doing so you are sure not to break any existing code. Basically if the client code doesn't call .AsKeyedService() everything will keep on working as before, with no support for keyed services resolution. In my opinion, this is the "least surprise" behavior.
If, instead, the client code does know that the concrete IServiceProvider implementation supports keyed services and if it does want to enable that support for the named caches, then it can do that by calling .AsKeyedService().
I'm not a huge fan of implicit behaviors, so I prefer this kind of api where I do need to explicitly require support for keyed services.

from fusioncache.

EnricoMassone avatar EnricoMassone commented on June 12, 2024 1

Hi @EnricoMassone and thanks for taking the time to answer in such a complete way!

Reflecting on your answer to my previous post, it seems to me that these features are just "weakly" connected: they are there to solve different needs

I don't know if I would call them "weakly connected", maybe more like "the features of keyed services are a subset of the ones of named caches" but yeah, I got your point and it makes sense.

and, as you pointed out, named caches are a more flexible approach since they are tied to the usage of compile time constants

I suppose you meant "they are NOT tied to the usage of compile time constants"? In that case, yes exactly.

If you put yourself in the user shoes, an implicit support to keyed service resolution to named caches comes at the cost of side effects:

  • one minor side effects is that, in any case, a new keyed service is added to the service collection. This keyed service is not explicitly registered by the user, it is there because of the named cache usage. I understand that this is needed in order to support the keyed service resolution for the named cache, but this seems to me an unexpected behavior.

Yes, technically. But also that is the reality anyway, meaning that for example when you call AddSignalR() you are effectively delegating the actual components registration to somebody else, and then it becomes an implementation detail (which may change in the future, and you have no control on).

  • one major side effect is the runtime exception which is the core of this discussion, that you get if the concrete IServiceProvider implementation does not support keyed services. This is a breaking change and a catastrophic one.

Agree.

Final point. Since you pointed out that the exception is thrown when the service provider is built from the service collection, there is no way for you to intercept this problem. Based on my understanding, the service provider is built when the host is built, usually inside the Program.cs file. Unless there is an extension point in the framework to inject logic to be executed at point in time (honestly, I have no idea and I have never heard of that), you have no way to handle this exception from the Fusion Cache code. In your post you mentioned the idea of feature detection, to check and see if the keyed service feature is supported by the runtime environment. Since the keyed service support logically belongs to the service provider, any feature detection mechanism would probably be associated with the IServiceProvider object: again, at the library level you interact with the IServiceCollection, since you are at service registration time and the host is still to be created.

Exactly.

For all of these reasons, I think that designing this feature (keyed services support for named caches) as an opt-in feature is even better than your initial idea of an out of the box implicit support.

99% agree, and this is how it is currently implemented in my local branch, and it is working well 🙂 I just need a little bit more time to marinate the idea and see if somebody else comes up with some notes.

Thanks again, much appreciated!

Good afternoon @jodydonetti you are welcome.

I suppose you meant "they are NOT tied to the usage of compile time constants"?
In that case, yes exactly.

Yes, there was a typo here. Actually what I meant is that named caches are not tied to the usage of compile time constants.

Of course, let's also wait for other people point of view. Designing public APIs is never easy. Anyway, joining this kind of discussions is a great opportunity to learn something new and to experience the challenges of designing a public library like this.

Keep on with the great job on Fusion Cache!

from fusioncache.

EnricoMassone avatar EnricoMassone commented on June 12, 2024 1

Hi @EnricoMassone , did you happen to take a look at or play with preview1? I'm about to release preview2 with some changes, any observation of that first preview would be helpful. Thanks!

Good evening, I just tried it in a little toy project and it seems to work fine.
I didn't have a chance to try it in real production code though.
Looking forward for the next stable release!

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Based on your explanation, you want that any code using a named cache registration should also be able to resolve the named cache instance by using the [FromKeyedServices attribute].

Ideally yes, I think this would've been great.

  1. why do you want to do that ? Why not simply waiting to see the final decision made for the IHttpClientFactory and copy the same approach in Fusion Cache ?

I uno-reverse the question: why should I wait?
In the same vein I would have waited for an hybrid cache instead of creating FusionCache.

  1. do you also want to support the reverse relationship ? I mean, being able to resolve a named cache from the IFusionCacheProvider service if a keyed service with the same name is registered on the service provider.

No: apart from the fact that having "named resolving to keyed resolving to named etc" would probably create an infinite loop, but the way to register FusionCache is via the AddFusionCache method, and not by manually registering a component by calling AddSingleton or AddKeyedSingleton or something else, so there's no need to support that.

Or, instead, you just want to make the named cache feature (which is a custom solution in Fusion Cache) to work well with the keyed service feature (which is the built-in solution proposed by Microsoft for basically the same problem) ?

Exactly this.

  1. what is your long term plan ? Do you want to maintain both features and make them working together, or instead your long term plan is to deprecate the named cache feature and promote the usage of keyed services instead ?

I don't see a need to deprecate named caches, so I'd like to keep supporting both (if you see a reason why let me know, I'm curious).

Also, the named caches approach is more powerful than the keyed one, since it allows programmatic access instead of declarative: basically the keyed approach is the simple and declarative version of the more flexible named approach.

By the way, I think that a very big advantage of the proposed opt-in approach is that
[...]
support for the named caches, then it can do that by calling .AsKeyedService().

Yep, exactly.

I'm not a huge fan of implicit behaviors, so I prefer this kind of api where I do need to explicitly require support for keyed services.

Got it, thanks for sharing!

from fusioncache.

aKzenT avatar aKzenT commented on June 12, 2024

I would like to offer another perspective. Basically your initial goal was that this keyed service approach will just work. Having to enable this feature explicitly is contrary to that goal as you have to explicitly search for such a method, which is unexpected.

Setting aside the problem of other DI implementations for a second, with the latest Microsoft DI, enabling keyed services by default should not be a breaking change as nothing will change unless you specifically asked for a keyed service. Agreed?

That leaves the issue of old Microsoft DI implementations or alternative DIs, which (for better or for worst) is the minority by now. In these DIs trying to register the services will cause an exception to be thrown. I would suggest to catch and wrap this exception in a custom exception that explains the problem and gives a clear instruction on how to opt out of this behavior.

As DI registration happens at startup, it is impossible to miss this change after updating Fusion cache, even if you missed the change in the documentation. Of course it's a small inconvenience for a small number of users, but so is requiring the majority of users to opt-in.

I think it's also fair to assume that the number of users using DIs that do not support keyed services will decrease over time, so I think it's better to use a solution that is prepared for this future.

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Hi @aKzenT , thanks for chipping in!

I would like to offer another perspective. Basically your initial goal was that this keyed service approach will just work. Having to enable this feature explicitly is contrary to that goal as you have to explicitly search for such a method

Agree.

which is unexpected

Not really: I mean people registering other keyed services manually know that they have to call AddKeyedEtc, and here they would call .AsKeyedService() .
Makes sense?

enabling keyed services by default should not be a breaking change as nothing will change unless you specifically asked for a keyed service. Agreed?

Nope, building the ServiceProvider will throw, that's the main issue.

Will answer later to the other points.

Update (regarding the other points you've made)

In these DIs trying to register the services will cause an exception to be thrown. I would suggest to catch and wrap this exception in a custom exception that explains the problem and gives a clear instruction on how to opt out of this behavior.

Eh, that was the first thought I had 😅, but... the exception is not thrown when adding the keyed service, but later on when building the service provider.
And there's nothing I can do there, sadly (but please someone correct me if I'm wrong).

As DI registration happens at startup, it is impossible to miss this change after updating Fusion cache, even if you missed the change in the documentation. Of course it's a small inconvenience for a small number of users, but so is requiring the majority of users to opt-in.

I thought about it (eg: "just let it explode in a potentially low number of cases"), but as I said I can't control it, throw/log some useful message or else, so that it would become a surprise and people would need to try to understand what is going on.
On the other hand consider these 2 things:

  • up until now, keyed services were not supported, so to get them a user would need to upgrade and ask for it, meaning they would not expect them to work without doing anything (even though I would've liked that...)
  • normally, when registering a keyed service, a user is already used/required to be explicit and call AddKeyedSingleton() instead of AddSingleton()

With this in mind and all things considered, I'm feeling it's the best approach.

Having said all of this, what do you think? I'm still open for new point of views.

from fusioncache.

EnricoMassone avatar EnricoMassone commented on June 12, 2024

Based on my understanding, your initial idea was to add an implicit support to keyed service resolution for named caches because, in a sense, it should be the natural expectation of a user: from a certain point of view, it feels natural being able to resolve a named cache as a keyed service.

Reflecting on your answer to my previous post, it seems to me that these features are just "weakly" connected: they are there to solve different needs and, as you pointed out, named caches are a more flexible approach since they are tied to the usage of compile time constants (which, instead, must be used with keyed services because of the FromKeyedServicesAttribute attribute).

If you put yourself in the user shoes, an implicit support to keyed service resolution to named caches comes at the cost of side effects:

  • one minor side effects is that, in any case, a new keyed service is added to the service collection. This keyed service is not explicitly registered by the user, it is there because of the named cache usage. I understand that this is needed in order to support the keyed service resolution for the named cache, but this seems to me an unexpected behavior. Given a code base making use of named caches, if the Fusion Cache library is updated to a newer version then the service collection is changed (because of the implicit keyed service): by definition, this is a breaking change (I agree that it is probably a corner case, but any code relying on the content of the service collection could be broken by this).
  • one major side effect is the runtime exception which is the core of this discussion, that you get if the concrete IServiceProvider implementation does not support keyed services. This is a breaking change and a catastrophic one.

To summarize, regardless of the runtime exception, the implicit support for keyed service with named caches is a breaking change to the library: the runtime exception simply makes it worse.

Final point. Since you pointed out that the exception is thrown when the service provider is built from the service collection, there is no way for you to intercept this problem. Based on my understanding, the service provider is built when the host is built, usually inside the Program.cs file. Unless there is an extension point in the framework to inject logic to be executed at point in time (honestly, I have no idea and I have never heard of that), you have no way to handle this exception from the Fusion Cache code.
In your post you mentioned the idea of feature detection, to check and see if the keyed service feature is supported by the runtime environment. Since the keyed service support logically belongs to the service provider, any feature detection mechanism would probably be associated with the IServiceProvider object: again, at the library level you interact with the IServiceCollection, since you are at service registration time and the host is still to be created.

For all of these reasons, I think that designing this feature (keyed services support for named caches) as an opt-in feature is even better than your initial idea of an out of the box implicit support. This way, the client code is in full control and can opt-in by explicitly calling .AsKeyedService().
A client code using a container with full support to keyed services could even decide not to opt-in this feature for whatever reason (maybe they simply don't care resolving named caches as keyed services or they don't want to use keyed services altogether): with the opt-in approach this can be done, with an implicit approach this is not possible.

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Hi @EnricoMassone and thanks for taking the time to answer in such a complete way!

Reflecting on your answer to my previous post, it seems to me that these features are just "weakly" connected: they are there to solve different needs

I don't know if I would call them "weakly connected", maybe more like "the features of keyed services are a subset of the ones of named caches" but yeah, I got your point and it makes sense.

and, as you pointed out, named caches are a more flexible approach since they are tied to the usage of compile time constants

I suppose you meant "they are NOT tied to the usage of compile time constants"?
In that case, yes exactly.

If you put yourself in the user shoes, an implicit support to keyed service resolution to named caches comes at the cost of side effects:

  • one minor side effects is that, in any case, a new keyed service is added to the service collection. This keyed service is not explicitly registered by the user, it is there because of the named cache usage. I understand that this is needed in order to support the keyed service resolution for the named cache, but this seems to me an unexpected behavior.

Yes, technically.
But also that is the reality anyway, meaning that for example when you call AddSignalR() you are effectively delegating the actual components registration to somebody else, and then it becomes an implementation detail (which may change in the future, and you have no control on).

  • one major side effect is the runtime exception which is the core of this discussion, that you get if the concrete IServiceProvider implementation does not support keyed services. This is a breaking change and a catastrophic one.

Agree.

Final point. Since you pointed out that the exception is thrown when the service provider is built from the service collection, there is no way for you to intercept this problem. Based on my understanding, the service provider is built when the host is built, usually inside the Program.cs file. Unless there is an extension point in the framework to inject logic to be executed at point in time (honestly, I have no idea and I have never heard of that), you have no way to handle this exception from the Fusion Cache code. In your post you mentioned the idea of feature detection, to check and see if the keyed service feature is supported by the runtime environment. Since the keyed service support logically belongs to the service provider, any feature detection mechanism would probably be associated with the IServiceProvider object: again, at the library level you interact with the IServiceCollection, since you are at service registration time and the host is still to be created.

Exactly.

For all of these reasons, I think that designing this feature (keyed services support for named caches) as an opt-in feature is even better than your initial idea of an out of the box implicit support.

99% agree, and this is how it is currently implemented in my local branch, and it is working well 🙂
I just need a little bit more time to marinate the idea and see if somebody else comes up with some notes.

Thanks again, much appreciated!

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Hi all, I just updated the original proposal, look for the "Update 2024-05-10" part at the bottom.

It's not about registering stuff but about adding support for resolving keyed registered services for things like the backplane, the distributef cache, etc.

Opinions?

from fusioncache.

amoerie avatar amoerie commented on June 12, 2024

Read the whole discussion, and I'm not sure if I missed this point, but the nice thing about AsKeyedServices is also that in a distant future, where all DI providers support this feature, you can simply make the method obsolete. Early adopters will hopefully keep reading changelogs, and the silent majority will be none the wiser.

from fusioncache.

EnricoMassone avatar EnricoMassone commented on June 12, 2024

Hi all, I just updated the original proposal, look for the "Update 2024-05-10" part at the bottom.

It's not about registering stuff but about adding support for resolving keyed registered services for things like the backplane, the distributef cache, etc.

Opinions?

Based on my understanding, with the current implementation of methods WithRegisteredXyz() the library looks for a specific service type in the DI container and registers a cache by using the service implementation registered in the DI container.

For instance, let's suppose we have:

services.AddFusionCache().WithRegisteredBackplane();

in this case, an instance of IFusionCacheBackplane is asked to the DI container and the implementation returned by the DI container is used to create a cache.

What happens, in the current version of Fusion Cache, if a user registers multiple implementations of the IFusionCacheBackplane service ?
For instance:

services.AddSingleton<IFusionCacheBackplane, FooBackplane>();
services.AddSingleton<IFusionCacheBackplane, BarBackplane>();
services.AddFusionCache().WithRegisteredBackplane();

Based on my understanding, in this case the last registration wins, so the resolved instance of IFusionCacheBackplane should be BarBackplane. So, the cache is crated by using BarBackplane as the backplane type. Am I correct ?

I'm asking this just to understand the actual use case to support keyed services for things like backplanes. If I get it right, with the current version there is not the possibility of registering different backplanes and then pick one of them to be used with the cache: as explained above, the last one registered wins and it is the one actually used.
So, this is basically a brand new feature (this situation is slightly different from the named caches one: in that case the feature of having named caches was already there and the keyed services support is just an enrichment / improvement).

Is the use case behind this new feature to let a user define multiple backplane implementations, give them a name, and use them with different caches (by picking the chosen one by name) ?

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Based on my understanding, with the current implementation of methods WithRegisteredXyz() the library looks for a specific service type in the DI container and registers a cache by using the service implementation registered in the DI container.

For instance, let's suppose we have:

services.AddFusionCache().WithRegisteredBackplane();

in this case, an instance of IFusionCacheBackplane is asked to the DI container and the implementation returned by the DI container is used to create a cache.

Correct.

What happens, in the current version of Fusion Cache, if a user registers multiple implementations of the IFusionCacheBackplane service ? For instance:

services.AddSingleton<IFusionCacheBackplane, FooBackplane>();
services.AddSingleton<IFusionCacheBackplane, BarBackplane>();
services.AddFusionCache().WithRegisteredBackplane();

Based on my understanding, in this case the last registration wins, so the resolved instance of IFusionCacheBackplane should be BarBackplane. So, the cache is crated by using BarBackplane as the backplane type. Am I correct ?

Yes, I'm not 100% sure since it's not that common, but I think so.

Anyway, it's basically a way to say "use THE backplane is that has been registered via DI".
Because of that, the behaviour is the one defined by the DI container (not FusionCache) when registering more than one service of type IFusionCacheBackplane and later calling serviceProvider.GetService<IFusionCacheBackplane>().
Makes sense?

I'm asking this just to understand the actual use case to support keyed services for things like backplanes.

The use case, in general, is to add support for keyed services everywhere it can make sense: at first it was about FusionCache registering itself to be later resolved by user code via [FromKeyedService("name")], but later I remembered that FusionCache itself was sometimes resolving services, so I wanted to think about supporting that scenario, too.

A concrete example I can think about for this scenario is registering 2 different IFusionCacheBackplanes via keyed services, and then being able to use them via keyed services.
Can this be done in another way? Yup, sure, but it's just about supporting a feature (in this case keyed services) everywhere it is possible, and let users pick and choose if and how to use it.

If I get it right, with the current version there is not the possibility of registering different backplanes and then pick one of them to be used with the cache:

Not directly, but it's possible to do this:

services.AddFusionCache()
	.WithBackplane(sp =>
	{
		var backplanres = sp.GetServices<IFusionCacheBackplane>();
		// TODO: USE A LOGIC HERE...
		return backplanres.First();
	});

So, this is basically a brand new feature

Kinda, we can say this.

Is the use case behind this new feature to let a user define multiple backplane implementations, give them a name, and use them with different caches (by picking the chosen one by name)?

Yes.

from fusioncache.

EnricoMassone avatar EnricoMassone commented on June 12, 2024

Based on my understanding, with the current implementation of methods WithRegisteredXyz() the library looks for a specific service type in the DI container and registers a cache by using the service implementation registered in the DI container.
For instance, let's suppose we have:
services.AddFusionCache().WithRegisteredBackplane();
in this case, an instance of IFusionCacheBackplane is asked to the DI container and the implementation returned by the DI container is used to create a cache.

Correct.

What happens, in the current version of Fusion Cache, if a user registers multiple implementations of the IFusionCacheBackplane service ? For instance:

services.AddSingleton<IFusionCacheBackplane, FooBackplane>();
services.AddSingleton<IFusionCacheBackplane, BarBackplane>();
services.AddFusionCache().WithRegisteredBackplane();

Based on my understanding, in this case the last registration wins, so the resolved instance of IFusionCacheBackplane should be BarBackplane. So, the cache is crated by using BarBackplane as the backplane type. Am I correct ?

Yes, I'm not 100% sure since it's not that common, but I think so.

Anyway, it's basically a way to say "use THE backplane is that has been registered via DI". Because of that, the behaviour is the one defined by the DI container (not FusionCache) when registering more than one service of type IFusionCacheBackplane and later calling serviceProvider.GetService<IFusionCacheBackplane>(). Makes sense?

I'm asking this just to understand the actual use case to support keyed services for things like backplanes.

The use case, in general, is to add support for keyed services everywhere it can make sense: at first it was about FusionCache registering itself to be later resolved by user code via [FromKeyedService("name")], but later I remembered that FusionCache itself was sometimes resolving services, so I wanted to think about supporting that scenario, too.

A concrete example I can think about for this scenario is registering 2 different IFusionCacheBackplanes via keyed services, and then being able to use them via keyed services. Can this be done in another way? Yup, sure, but it's just about supporting a feature (in this case keyed services) everywhere it is possible, and let users pick and choose if and how to use it.

If I get it right, with the current version there is not the possibility of registering different backplanes and then pick one of them to be used with the cache:

Not directly, but it's possible to do this:

services.AddFusionCache()
	.WithBackplane(sp =>
	{
		var backplanres = sp.GetServices<IFusionCacheBackplane>();
		// TODO: USE A LOGIC HERE...
		return backplanres.First();
	});

So, this is basically a brand new feature

Kinda, we can say this.

Is the use case behind this new feature to let a user define multiple backplane implementations, give them a name, and use them with different caches (by picking the chosen one by name)?

Yes.

Ok, now it makes more sense to me. Thanks !

In general, this seems to be a good idea. My only point is about this overload:

services.AddFusionCache("MyCache")
  .WithRegisteredKeyedBackplane();

Is this really needed ? Probably your rationale is that in many cases the user will register a named backplane having the very same name of the cache. Correct ?
What do you expect to happen if this overload is used in this way:

services.AddFusionCache()
  .WithRegisteredKeyedBackplane();

In this case you don't have a cache name, at least you don't have an explicit one (I don't know if there is an implicit default cache name in this case).

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

In general, this seems to be a good idea. My only point is about this overload:

services.AddFusionCache("MyCache")
  .WithRegisteredKeyedBackplane();

Is this really needed? Probably your rationale is that in many cases the user will register a named backplane having the very same name of the cache. Correct?

Correct.

What do you expect to happen if this overload is used in this way:

services.AddFusionCache()
  .WithRegisteredKeyedBackplane();

In this case you don't have a cache name, at least you don't have an explicit one (I don't know if there is an implicit default cache name in this case).

Any cache has a name underneath, including the default one: for that, the name is FusionCacheOptions.DefaultCacheName.

Anyway I see your point, and in fact I'm still not sure about the parameterless version: on one hand it makes sense to specify the name just once (with AddFusionCache("name")) and then simply tell it to look for the the keyed backplane/distributed cache/whatever without repeating it, but on the other hand it may create doubts... I don't know, still have to marinate the idea a little bit.

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Hi all, I just released v1.2.0-preview1 🥳

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Hi @EnricoMassone , did you happen to take a look at or play with preview1?
I'm about to release preview2 with some changes, any observation of that first preview would be helpful.
Thanks!

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Thanks @EnricoMassone !
Since preview1 I slightly changed it to better fit with the existing keyed services in .net, will update with the next release.

from fusioncache.

jodydonetti avatar jodydonetti commented on June 12, 2024

Hi all, FusionCache v1.2.0 is out 🥳

from fusioncache.

Related Issues (20)

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.