Shiv
Shiv

Reputation: 1404

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

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.

Upvotes: 30

Views: 5356

Answers (9)

Przemysław R
Przemysław R

Reputation: 107

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...

Upvotes: 0

Drew Noakes
Drew Noakes

Reputation: 310937

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>

Upvotes: 3

Mike Nakis
Mike Nakis

Reputation: 61993

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.

Upvotes: 2

Ben
Ben

Reputation: 1605

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.

Upvotes: 23

Tomas Kubes
Tomas Kubes

Reputation: 25128

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.

Upvotes: 3

Brett Postin
Brett Postin

Reputation: 11375

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

Upvotes: 8

Dylan Smith
Dylan Smith

Reputation: 22245

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

Upvotes: 8

Brett Postin
Brett Postin

Reputation: 11375

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.

Upvotes: 4

Gayot Fow
Gayot Fow

Reputation: 8792

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.

Upvotes: 2

Related Questions