Make IoC/DI friendlier#108
Make IoC/DI friendlier#108haacked merged 27 commits intoscientistproject:masterfrom martincostello:Static-To-Not
Conversation
Push the global IResultPublisher instance from Scientist through to ExperimentInstance via ExperimentSettings.
Make each InMemoryResultPublisher instance have its own state, rather than each instance just wrap shared static state.
Add a "Professor" class, which provides the same functionality as Scientist, but as an instance-based class, rather than a static class. This is to make activation via dependency injection easier by removing the need to set a shared static property with the desired IResultPublisher instance that might require activation via an IoC container.
Add the ItemGroup that Visual Studio 2015 and later insist on adding to test projects.
|
I took a very brief look at this and overall I think it's a good change and I can see it being useful. (I'm hoping to circle back in a few hours and take a deeper look but we'll see how my day goes.) My only comment right now is about Professor - it looks like a complete, but non static, copy of Scientist. I hate having that duplication because it will make maintenance a pain, and potentially adds confusion for any devs trying to use it. Can we somehow get away with a single implementation of Scientist to achieve both functionalities? Perhaps a non static class with a static |
|
Yeah that's definitely an approach we could take, and yeah it pretty much is a copy. For now, I did that as a way of not making a breaking change to the interface and make the diff a bit cleaner for conceptual review. Bumping the version to |
|
I like where this is heading. Here's a thought that I don't think would require breaking changes. The way the However, for developers who plan to always have experiments running and have invested in existing DI and IoC, I definitely see the benefit of making the library more friendly to those approaches. So here's a proposal:
This way, existing users code will continue to just work. But users who want to use DI/IoC will register and import an We could consider deprecating all the static methods later if we wanted to. |
|
I did consider doing the "make the static defer to a singleton instance" approach, but I thought I'd get the conversation started first as that was more work than the initial "Professor" commit. I like the proposal above @haacked, I guess we'd just have to figure exactly what we want to make as the interface members, and what would be convenience extras. For example, we could make the methods that take the I'm not sure whether or not making a static class not is a breaking change from a binary drop-in point-of-view for the IL of existing calls, but yeah it certainly could be a non-breaking code change for upgrade and recompile. I think the only thing that would need a bit more thought is how to support the changing the |
Bump AppVeyor CI OS to VS 2017 and the .NET Core SDK to 2.1.200 to see if that fixes the build not running propertly anymore.
Yeah, I think this is right. Note that under my proposal, if you set up IoC in a project as your means to using Scientist, but you accidentally call a static method, you might be operating on two different instances of Scientist. So it's important that you stick with one approach or another. That's probably fine for now. |
|
I’ll update this PR with a first cut of the proposed updates tomorrow. |
Update .NET Core test SDK and xunit to the latest stable versions. Bump tests to target netcoreapp2.0.
Try and fix the AppVeyor build by adjusting the indenting for the build script.
|
The AppVeyor CI appears to be ignoring the custom build script and is just trying to run vanilla MSBuild on the solution file instead. I've tried to fix it, but I'm a bit baffled as to what's going on... |
|
I'm not sure how the diff on this pull request is showing the previous version of the appveyor file to have weirs things like double hyphens but if you look at the original file compared to what you've got, you should be able to restore the yml formatting https://github.com/github/Scientist.net/blob/master/appveyor.yml |
Refactor Scientist to support creating instances to support use with IoC containers. Existing static methods defer to a private singleton instance of Scientist. Add IScientist which contains the two most configurable methods from Scientist, and add extension methods to add the convenience overloads.
Reset the AppVeyor YAML file back to master.
|
@ryangribble I've reset the YAML file, but while the build appears to be green, if you look at it it doesn't actually build anything. It's as if AppVeyor is completely ignoring the existence of the file. |
Explicitly use the Visual Studio 2017 AppVeyor image.
|
It looks like it's ignoring the |
|
hmm ok, i was going to say perhaps in the appveyor settings the "ignore appveyor.yml" option is enabled (only @haacked would be able to confirm that) however if that change is having an effect I guess that wouldn't be it |
Test a theory about the AppVeyor YAML by adjusting the formatting and only running one build command for now.
Restore the commands to run the tests and create the package to AppVeyor YAML.
|
I think maybe that the |
Fix incorrect order of parameters to exception in Scientist constructor.
|
@haacked Have done an initial pass at changing |
Fix the state of Scientist instances not being used to build experiments.
Add unit tests for using instances of Scientist to run experiments.
Remove Swap.Publisher() as it is no longer used.
Refactor an instance unit test to not depend on the shared results publisher.
haacked
left a comment
There was a problem hiding this comment.
Just a couple nitpicky things. Overall looks good.
src/Scientist/Scientist.cs
Outdated
| public Scientist(IResultPublisher resultPublisher) | ||
| { | ||
| if (resultPublisher == null) | ||
| throw new ArgumentNullException(nameof(resultPublisher), "A result publisher must be specified"); |
There was a problem hiding this comment.
Here's an opportunity to use the new C# throw expressions to make the code even simpler.
_resultPublisher = resultPublisher
?? throw new ArgumentNullException(nameof(resultPublisher), "A result publisher must be specified");
src/Scientist/Scientist.cs
Outdated
| /// <returns>The value of the experiment's control function.</returns> | ||
| public static Task<T> ScienceAsync<T>(string name, Action<IExperimentAsync<T>> experiment) => | ||
| ScienceAsync(name, 1, experiment); | ||
| _sharedScientist.Value.ExperimentAsync(name, experiment); |
There was a problem hiding this comment.
Looks like a stray character of whitespace was added here.
There was a problem hiding this comment.
There were a couple of these in this class, all adjusted now.
Make parameter validation more terse. Remove rogue whitespace.
|
Assuming no further code feedback, I think the last things to do are:
|
|
Awesome changes - I'm excited to see improved integration with DI! |
Update to the .NET Core 2.1 SDK.
Add support for SourceLink so users can step-into Scientist itself in the applications they integrate it into.
Remove redundant arguments from the build scripts.
Bump version to 2.0.0.0 (alpha1).
Add bare-bones entry for 2.0.0 to ReleaseNotes.md.
|
I can't remember if I have this set up to automatically publish to NuGet on merge to Thanks for the contribution @martincostello! |
|
Looks like AppVeyor moved the 🧀 since the last changes to Scientist @haacked, so the NuGet packages aren't being hoovered up in the build for publishing. |
|
sigh I need to spend some time looking into this. |
|
@haacked I appreciate you've got plenty on, just curious on some rough timelines for |
|
@martincostello I just published a 2.0.0-beta to NuGet (it's still validating right now). Please try it out and if it works for you, I'll publish a stable version of the package. Thanks! |
|
@haacked Awesome! I’ll give it a spin on Monday. |
|
Great! It's available now if anyone else wants to give it a spin. |
|
I pull it later this weekend, and mess around with it. |
|
@haacked I've pulled the |
|
Thanks for the confirmation @martincostello! I published a stable version of 2.0.0 today. https://haacked.com/archive/2018/06/05/scientist-2-point-oh/ |
|
Awesome - thanks for your help @haacked! |
|
@martincostello do you happen to remember why you needed to subclass Scientist instead of just doing |
|
I don't remember, but the comment on the class says "This class acts as a proxy to allow the static methods to set the state on an instance of Scientist." |
|
I do see that, but based on #129, it would seem that things work even without the sub class |
|
I didn't want changes to the global shared stuff to affect other instances as having global shared state (especially when you're trying to add in DI to make everything nice and isolated and testable), so that was mainly there to provide a measure of backwards compatibility to people using the old static way of doing things without affecting instances created the new way. I think it came about from testing, because if tests ran in parallel they'd cause things to randomly fail when the shared state was being altered. |

This PR is very much a proposal for discussion rather than a fait accompli, so is presented more in a "does the job" fashion.
This week I plugged Scientist into an ASP.NET Core application for use with running an experiment on part of the code, and as such would have liked to register my
IResultPublisherfor use via dependency injection so it is only activated when needed and my implementation can be activated the same way for its dependencies. However, due to the shared staticScientist.ResultPublisherproperty, this isn't really possible to do in a nice way.In the end I settled on setting up
IResultPublisherin IoC as a singleton where the factory method to create the service set the resolved implementation as the value ofScientist.ResultPublisherbefore it returned, and then made the code where I wanted to run the experiment have an unusedIResultPublisherconstructor parameter to force the IoC container to set it up so it was created on-demand.This PR introduces a new
Professorclass which is instance-based, rather than static, and provides the same functionality as theScientistclass. The name of the class is arbitrary and I just picked something "sciency" that didn't need the existingScientistclass to be changed and be a breaking change.Professoraccepts anIResultPublisheras a constructor parameter, allowing the running of the experiment to be decoupled from ownership and activation of the publisher itself, so it can be managed by an IoC container, for example. It also moves the concurrent tasks to a property to reduce the number of methods present on the class, but the overloads could always be restored for parity.Below are some example snippets of code from an ASP.NET Core 2.1 application leveraging
Professorto run experiments using DI for requests to a controller that are published to another HTTP service.I've also changed
InMemoryResultPublisherto be backed by instance fields so that each instance has its own results. This was mainly to make it easier to duplicate the tests forScientistforProfessorwithout the state clashing. This change could be reverted, but would require some rework in the tests forProfessorto use a different default results implementation so it doesn't affect theScientisttests.