Is there a way to prevent certain references from being included on a project?
Asked Answered
S

9

30

Basically, I want to do some preventative maintenance. There are certain third-party libraries that I'd like to prevent being included as references in a certain project. Is there a way you can specify which references are prohibited for a project?

The project I want to protect is a class library that I want to have functionality from a very specific set of third-party libraries. The class library is used in other solutions for common data access functionality, so if those third-party libraries were referenced, they would be needed as well. The aim is to keep that one project just a data access library and keep the "package" lightweight.

Single answered 21/6, 2013 at 1:20 Comment(9)
I assume you don't just mean 'remove the references' but rather 'prevent them from ever being re-added' ?Stavro
Who are these rogue colleagues of yours adding references to forbidden projects? You should probably sort this out IRL without resorting to draconian measures such as this.Propane
Mandatory code reviews can help.Cyperaceous
Yes. Remove the 3rd party libraries from all computers that your developers use and don't allow them to be available to add in the first place. Other than that, no. They're developers, so they have access to the source code, remember?Laminated
Add it to their employment contract.Rigveda
The DLLs can be added to other projects. I just want to prevent them being added to a certain project as that particular project is a class library that is imported into other solutions.Single
@KirkWoll There isn't a particular person I am concerned with. I have just performed the refactoring to move the 3rd party library dependencies out. I just want to make sure they won't be re-added again later without having to watch it like a hawk :)Single
@KirkWoll relying on humans to enforce architectural constraints when it can be automated sounds pretty draconian to me. Training and reviews will of course help in the first instance, but mistakes will inevitably happen.Rodenticide
I can certainly see the value in this. In large teams, especially where code reviews only happen between a team they can circumvent preventative measures by accident and cause issues that go uncaught. This would be no different than validating functionality, but rather validating architecture.Badly
D
23

I work in a large development team, all working on the same software, and have a similar issue.

We work in a large domain-driven design (DDD) architecture with many different bounded contexts and don't want to people to add references between the contexts.

We have guidelines, standards, architecture documents, code reviews etc., lots of things which prevent references of being added IRL (as somebody put it). However, we had two relatively new starters who haven't got much experience with the current structure and just don't magically know everything. They happened to review each others code and voilà, the unwanted reference is added.

I see nothing draconian in trying to prevent mistakes from happening and making sure standards are adhered to. Just a precautionary measure. Isn't that partially why we are writing unit tests, too? So that some other, new guys/gals in the future can be made aware that they unknowingly broke something?

I don't particularly like analysing the project file for the references. The way we'll probably handle it is to define a set of unit tests which crawl through the assembly references of every project under test and fail when they identify references which aren't supposed to be there.

Obviously that only works if you have continuous integration / deployment including running the unit tests.

So even if the new guys/gals check in some stuff without running the unit tests locally first (and realising their mistake), our bright red blinking build status light or the build server emails will soon tell everybody on the team what has gone wrong.

Delicacy answered 4/9, 2015 at 5:4 Comment(1)
Ben, I would have very much liked to upvote this answer, but by the end of it I realized that it does not actually provide a solution. If, in the 5 years that intervened, you have implemented something, please post some code!Quartzite
B
8

You can use Visual Studio Layer Diagrams to achieve this (assuming you have Visual Studio Ultimate). Draw a box/layer representing your project, drop your project onto it to link them, draw a box/layer representing forbidden assemblies, drop those assemblies onto it to link them, and you're done (by not drawing a dependency arrow between the layers, you're indicating to Visual Studio that dependencies aren't allowed).

Now turn on Layer Diagram Validation in the project and/or TFS Build by setting the MSBuild property: ValidateArchitecture=True

Blindfold answered 21/6, 2013 at 2:42 Comment(1)
In truth this won't technically prevent them from adding the reference, but it will prevent them from ever using that reference.Blindfold
R
8

This is definitely a valid question and Ben's answer pretty much hits the nail on the head. However, this tool can help automate the enforcement of your reference constraints:

NsDepCop

Rodenticide answered 17/5, 2018 at 8:54 Comment(1)
I hadn't heard of this tool before - I had it installed and working in 5 minutes, good suggestion!Frenchy
S
3

Write an extension for the Roslyn compiler with checking all your rules and put this Analyzer (as a NuGet package) inside the solution. Your analysis will be part of the compilation.

This is very similar to how NsDepCop basically works.

Sheridan answered 21/1, 2019 at 16:10 Comment(2)
This is certainly an option, however including directly in solution (as opposed to nuget package or vsix) isn't well documented and can be quite fiddly to setup. You also need to be careful not to degrade IDE performance with inefficient analysers.Rodenticide
I meant to create Nuget and include it in the solution, I was too brief.Sheridan
R
3

Having revisited this several years after my other answer, I discovered this library:

ArchUnitNET

It is a .NET port of ArchUnit for Java that allows you to write unit tests using a fluent API to describe your architecture constraints.

So you can write things like:

IArchRule rule = Types().That().ResideInNamespace("Model").Should()
                    .NotDependOnAny(Types().That().ResideInNamespace("Controller"));

Currently using this in practice on a large DDD based solution to enforce project references and also monitor introduction of nuget packages.

Rodenticide answered 22/9, 2022 at 11:50 Comment(0)
J
3

You can prevent projects from building if they have certain references (whether direct assembly references, project references or package references) by sticking something like this in a Directory.Build.targets file in the root of your repo:

<Project>

  <Target Name="ValidateDisallowedReferences" BeforeTargets="CoreCompile">
    <Error
      Condition="'%(Reference.FileName)' == 'Newtonsoft.Json'"
      Text="Newtonsoft.Json is not supported. Please use System.Text.Json instead." />
  </Target>

</Project>
Jacobba answered 27/1, 2023 at 11:41 Comment(0)
A
2

It might be more practical to search for the permitted assemblies and flag up the exceptions, because somebody could simply rename a rogue assembly to a name that's not on your list and escape detection.

The .csproj file used to build an assembly is a plain-vanilla XML file, so the referenced assemblies can be easily located with an XPath statement where the predicate(s) are the permitted assembly names. You could set up a trigger that whenever a .csproj file is checked in to the source repository the file is scanned and any culprit assemblies are flagged up.

Using this approach, or any similar approach, carries the risk that you're inviting the rogue developers to play a game of leap-frog. And you're likely to lose that game because developers are superb leap-frog players. So a more robust approach would be to rely less upon technology and more upon a programme of managerial recourse.

Antiknock answered 21/6, 2013 at 2:31 Comment(1)
In this case, the DLLs I'm concerned about are specific 3rd party libraries so there is a fixed list.Single
Q
2

This is an excellent question, and its applicability is broader than the author may have anticipated.

Many a WPF project has turned into spaghetti goo because of n00b programmers who do not quite understand MVVM, and the importance of separation between application logic and presentation logic.

Most WPF programmers put views and viewmodels side by side in the same directory. Every single WPF project I have ever seen is structured like that. It may be convenient because the source files are always next to each other, but it is dead wrong, because it means that both views and viewmodels reside in the same project, which means that the WPF assemblies are available to application logic, which means that there is no separation between application logic and presentation logic. (Which is what MVVM was mainly invented to address.)

That's how you get tests running on a CI/CD server eternally stuck waiting for someone to click OK on an application-modal message box, and other hilarious situations like that.

To prevent this, it is prudent to put all application logic in a separate project, and refrain from having that project reference any WPF assemblies. This may necessitate some infrastructure work, like abstracting the MessageBox facility, the Dispatcher, etc. and providing (injecting) these abstractions to the application logic, but it is very well worth doing.

Once you have accomplished this separation between application logic and presentation logic, the next question is how to prevent n00b programmers from mixing them together again.

For example, as soon as someone needs to work with the concept of colors in application logic, they won't even blink before referencing the WPF assemblies to start making use of System.Windows.Color. The question is how to prevent that.

Other answers to this question suggest using third-party tools, using code analysis, and even parsing project files. User "Ben" wrote in his answer that he solved the problem programmatically, but he did not show any code.

So, I had to write the code. Here is how I did it:

Assert( noProhibitedAssembliesAreReferencedAssertion() );
.
.
.
    private static bool noProhibitedAssembliesAreReferencedAssertion()
    {
        ImmutableList<string> names = Assembly.GetExecutingAssembly()
                .GetReferencedAssemblies()
                .Select( a => a.Name )
                .Where( isProhibitedAssemblyName )
                .ToImmutableList();
        if( names.Count > 0 )
        {
            dumpReferencedAssembliesOfModule( "Executing assembly (should not reference prohibited assemblies)",
                    Assembly.GetExecutingAssembly() );
            dumpReferencedAssembliesOfModule( "Calling assembly (can reference any assembly)",
                    Assembly.GetCallingAssembly() );
            throw new AssertionFailureException( $"The application logic references prohibited assemblies: {names.MakeString( ", " )}" );
        }
        return true;
    }

    private static readonly string[] prohibitedAssemblyNames =
        {
            "System.Windows", //
            "PresentationFramework", //
            "WindowsBase", //
            "System.Xaml", //
            "PresentationCore", //
            "System.Printing", //
            "ReachFramework", //
            "MahApps.Metro", //
            "SciChart.Charting", //
            "SciChart.Charting3D", //
            "SciChart.Core", //
            "SciChart.Drawing", //
            "SciChart.Data"
        };

    private static bool isProhibitedAssemblyName( string assemblyName )
    {
        return prohibitedAssemblyNames.Contains( assemblyName );
    }

    private static void dumpReferencedAssembliesOfModule( string message, Assembly assembly )
    {
        logger.Info( $"{message}: {assembly.FullName}" );
        logger.Info( "     GetModules():" );
        foreach( Module module in assembly.GetModules() )
            logger.Info( $"        {module.Name} {module.Assembly}" );
        logger.Info( "     GetReferencedAssemblies():" );
        foreach( var referencedAssembly in assembly.GetReferencedAssemblies() )
            logger.Info( $"        {referencedAssembly.Name}" );
    }

The above code sits in the main application logic object, (your MainViewModel or equivalent,) and checks which assemblies have been referenced by the application logic assembly. If it finds any "prohibited" assemblies, it deliberately fails. This code runs during application startup, but also in application logic tests, when the tests instantiate the MainViewModel.

Be sure to accompany this code with great big huge warning comments telling anyone that if it fails, they should contact you before changing anything.

Replacing assembly names with regular expression patterns is left as an exercise to the reader.

Quartzite answered 1/6, 2022 at 10:15 Comment(0)
L
0

For the future people entering this post and looking for preventing to reference project not 3rd party libraries:

<Target Name="ValidateDisallowedReferences" BeforeTargets="CoreCompile">
  <Error
    Condition="'%(ProjectReference.FileName)' == 'Disallowed name' and !($(MSBuildProjectName.StartsWith('ProjectNamesExcluded1')) or $(MSBuildProjectName.StartsWith('ProjectNamesExcluded2')))"
    Text="You shouldn't reference XYZ projects! " />
</Target>

I spend many hours to figure this out...

Lierne answered 11/5 at 17:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.