Category: projects

NetCoreUsefullEndpoints-part 14–adding roles and claims and authorization

I have added the current user role and claims to the nuget Usefull Endpoints for .NET Core  .  The endpoints are

api/usefull/user/isInRole/{roleName}

and

api/usefull/user/claims/simple

and

api/usefull/user/authorization

For the first one the code is pretty simple

 route.MapGet("api/usefull/user/isInRole/{roleName}", (HttpContext httpContext, string roleName) =>
        {            
            return httpContext.User?.IsInRole(roleName)??false;

        }).WithTags("NetCoreUsefullEndpoints")
        .WithOpenApi();

For claims and authorization , please see code at ignatandrei/NetCoreUsefullEndpoints: Usefull Endpoints for .NET Core

Demeter and Roslyn–part 3–config and tests


Beyond the Squiggles: Flexible Reporting and Confidence Through Testing

So, we’ve built our Law of Demeter analyzer, RSCG_Demeter, using Roslyn to intelligently spot those overly-chatty code chains, even navigating the complexities of fluent interfaces. We could just have it throw warnings or errors directly in your IDE via Roslyn diagnostics.

But let’s be honest, sometimes a flood of new diagnostics can feel a bit… harsh, especially when introducing a new rule to a large codebase. Maybe you want a summary report instead, something you can review offline or integrate into a CI process?

Flexibility First: Sending Demeter Violations to a File

I wanted to offer more control. What if you could just get a list of potential violations written to a file? Good news – you can!

RSCG_Demeter allows you to configure file output directly in your .csproj. Just add these settings:

<!-- In your .csproj file -->
<PropertyGroup>
  <!-- Make the build property visible to the analyzer -->
  <CompilerVisibleProperty Include="RSCG_Demeter_GenerateFile" />
</PropertyGroup>

<PropertyGroup>
  <!-- Define the output file path (relative to the csproj) -->
  <!-- Example: Output to 'DemeterReport.txt' in the project's parent directory -->
  <RSCG_Demeter_GenerateFile>../DemeterReport.txt</RSCG_Demeter_GenerateFile>
</PropertyGroup>

With this configuration, instead of diagnostics, the analyzer will neatly output all identified potential violations to the specified file.

How does it work under the hood? The analyzer uses Roslyn’s AnalyzerConfigOptionsProvider to read this custom MSBuild property. It even cleverly grabs the project directory (build_property.ProjectDir) to correctly resolve relative paths:

// Simplified snippet showing how the analyzer reads the setting
var writeToFile = context.AnalyzerConfigOptionsProvider.Select((provider, ct) =>
{
    // Try reading the custom build property
    provider.GlobalOptions.TryGetValue("build_property.RSCG_Demeter_GenerateFile", out var filePath);

    if (!string.IsNullOrWhiteSpace(filePath))
    {
        // Handle relative paths using the ProjectDir property
#pragma warning disable RS1035 // Analyzer correctness rule
        if (!Path.IsPathRooted(filePath) &amp;&amp;
            provider.GlobalOptions.TryGetValue("build_property.ProjectDir", out var csproj) &amp;&amp;
            !string.IsNullOrWhiteSpace(csproj))
        {
            filePath = Path.GetFullPath(Path.Combine(csproj, filePath));
        }
#pragma warning restore RS1035
        return filePath; // Return the absolute path
    }
    return string.Empty; // No file path configured
});

// Later, the analyzer checks if 'writeToFile' has a value
// and writes the report instead of creating diagnostics.

This gives you the flexibility to choose how you want to consume the Demeter analysis results – immediate feedback via diagnostics or a consolidated report via file output.

Building Confidence: It’s Tested!

An analyzer is only useful if it’s reliable. How do we know RSCG_Demeter correctly identifies violations and doesn’t flag valid patterns like our fluent builders? Testing!

The repository includes a dedicated test project (RSCGDemeter_Tests) packed with unit tests covering various scenarios, including:

  • Simple violations

  • Fluent interface patterns (return this)

  • Complex LINQ chains

  • Edge cases

These tests ensure the logic we discussed – from the initial dot counting to the semantic analysis of return types – works as expected.

Ready to Give it a Go?

Whether you prefer direct IDE feedback or a generated report, RSCG_Demeter aims to provide a robust and flexible way to encourage adherence to the Law of Demeter in your C# projects.

Check out the code, explore the tests, and try it on your solution!

➡️ https://github.com/ignatandrei/RSCG_Demeter

Let’s write less coupled, more maintainable C# together!


Demeter and Roslyn–part 2- code

Okay, let’s build on the first part and explain the “how” in a more engaging way, weaving in the code snippets to illustrate the process.


From Counting Dots to Understanding Code: Building the Demeter Analyzer

In the first part, we established our mission: build a Roslyn analyzer to sniff out Law of Demeter violations – stopping our code from “talking to strangers.” We also saw why the naive approach of just counting dots (.) runs into trouble with common patterns like fluent builders.

So, how do we get smarter about this?

Step 1: The Initial Scan (Finding Potential Candidates)

First, we need to find any method call chains that might be violations. We’re looking for code with multiple dots. Roslyn’s SyntaxProvider is perfect for this initial trawl. We tell it to look for InvocationExpressionSyntax (which represents a method call like DoSomething()) and then apply a quick filter:

// Using Roslyn's SyntaxProvider to find candidates

var lines = context.SyntaxProvider.CreateSyntaxProvider(
    // 1. Quick Check: Is it a method call?
    predicate: (sn, _) => sn is InvocationExpressionSyntax,
    // 2. Basic Filter: Does it have more than one dot?
    transform: (ctx, _) => FilterByDotCount(ctx)
)
.Where(it => it != null) // Filter out nodes that didn't pass
.Select((it,_) => it!); // Non-null assertion

// Helper to perform the simple dot count filter
private InvocationExpressionSyntax? FilterByDotCount(GeneratorSyntaxContext ctx)
{
    // We only care about InvocationExpressionSyntax
    if (!(ctx.Node is InvocationExpressionSyntax node)) return null;

    // Get the full text of the call chain (e.g., "a.b.c()")
    var text = node.ToFullString();
    if (string.IsNullOrEmpty(text)) return null;

    // Super basic check: count the dots.
    // If there's less than 2 dots (e.g., "a.b()" has 1), ignore it for now.
    var noDot = text.Replace(".", "");
    if (text.Length - noDot.Length < 2) return null; // Need at least two dots

    // It *might* be a violation, let's analyze further
    return node;
}

This gives us a list of syntax nodes that look suspicious based purely on the number of dots. But we know this includes false positives like builder.SetName(“X”).SetAge(30).

Step 2: The Semantic Deep Dive (Are We Talking to Ourselves?)

Now for the clever part. For each candidate identified above, we need to figure out if it’s a genuine violation or just a fluent interface talking to itself. This is where Roslyn’s Semantic Model comes in – it allows us to understand the meaning and types behind the syntax.

Here’s the core logic:

  1. We walk backward through the chain (from right to left).

  2. For each method call (InvocationExpressionSyntax) or property access (MemberAccessExpressionSyntax) in the chain, we use the semantic model to determine the return type.

  3. We keep track of the types we’ve seen in this specific chain.

  4. Crucially: If a method call returns the same type as a previous call in the same chain, we assume it’s a fluent pattern (like return this; or returning the same builder type). In this case, we don’t count the dot associated with that call as contributing to a Demeter violation.

// (Inside the main analysis loop for each candidate 'invocation')
List<ITypeSymbol> types = new(); // Track return types within this chain
bool isProblem = false;
int nrDots = 0; // Our adjusted dot count

// Get the starting point of the chain (e.g., 'a.b' in 'a.b.c()')
SyntaxNode? currentExpression = invocation.Expression;

// Walk the chain backwards (right-to-left)
while (currentExpression != null)
{
    ITypeSymbol? currentReturnType = null;

    // Is this part of the chain a method call? (e.g., the 'b()' in 'a.b().c()')
    if (currentExpression is InvocationExpressionSyntax inv)
    {
        // Use the Semantic Model to find the return type of this call
        currentReturnType = GetReturnTypeSymbol(semanticModel, inv); // Simplified helper call

        // Move to the next part of the chain (e.g., 'a' in 'a.b()')
        currentExpression = inv.Expression;
    }
    // Is this part of the chain a property/field access? (e.g., the '.b' in 'a.b.c()')
    else if (currentExpression is MemberAccessExpressionSyntax ma)
    {
        // We count this dot initially
        nrDots++;
        // Use the Semantic Model to find the type of the member being accessed
        currentReturnType = GetMemberTypeSymbol(semanticModel, ma); // Simplified helper call

        // Move to the next part of the chain (e.g., 'a' in 'a.b')
        currentExpression = ma.Expression;
    }
    else
    {
        // Reached the start or an unexpected syntax, stop walking
        break;
    }

    // --- The Fluent Interface Check ---
    if (currentReturnType != null)
    {
        // Have we seen this return type *before* in this specific chain?
        // We use SymbolEqualityComparer.Default for accurate type comparison.
        if (types.Contains(currentReturnType, SymbolEqualityComparer.Default))
        {
            // Yes! This looks like a fluent builder pattern.
            // Decrement the dot count we added earlier for this step.
            nrDots--;
        }
        else
        {
            // First time seeing this type in the chain, add it to our list.
            types.Add(currentReturnType);
        }
    }
} // End while loop walking the chain

// After checking the whole chain, is the adjusted dot count too high?
// (We generally consider > 1 adjusted dots a violation)
isProblem = nrDots > 1;

if (isProblem)
{
    // Report the violation! (Code to create diagnostic omitted for brevity)
    // ... uses invocation.GetLocation(), nrDots, etc.
}

By using the semantic model to track return types, we can intelligently discount the dots involved in fluent chains, giving us a much more accurate picture of potential Law of Demeter violations.

Try It Out!

This combination of initial syntax filtering and deeper semantic analysis allows RSCG_Demeter to flag overly chatty code while respecting common C# idioms.

Want to see it in action on your own codebase? Head over to the GitHub repository:

➡️ https://github.com/ignatandrei/RSCG_Demeter

Install the analyzer and see if your code is respecting the “Don’t talk to strangers” rule! Feedback and contributions are welcome!

Demeter and Roslyn–part 1–idea

Stop Your C# Code Talking to Strangers: Introducing a Law of Demeter Roslyn Analyzer

The Law of Demeter (LoD), often summarized as “Only talk to your immediate friends,” is a valuable principle for writing loosely coupled, maintainable code. Code that follows LoD tends to be less fragile and easier to refactor.

But manually checking for LoD violations? That’s tedious. Why not automate it?

That’s why I built RSCG_Demeter: a Roslyn Source Code Generator and Analyzer designed to detect potential Law of Demeter violations directly in your C# code.

Why It’s Tricker Than Just Counting Dots

While the simplest LoD violations look like obj.GetThing().GetAnotherThing().DoWork() (too many dots!), real-world C# throws curveballs:

  1. Fluent Interfaces (return this): Code like builder.SetName(“X”).SetAge(30) chains calls, but it’s calling methods on the same object. This is valid LoD, not talking to strangers! The analyzer needs to recognize this pattern.
    // Example: EmpBuilder returning 'this'
    public EmpBuilder SetName(string name) { /*...*/ return this; }
    // Usage: new EmpBuilder().SetName("Alice").SetRole("Dev");
    

     

     

  2. Complex LINQ Chains: Analyzing expressions like
     empAll.Select(it => it.ID).Distinct().OrderBy(it => it)
    var ids = new List<int>(empAll.Select(it => it.ID).Distinct().OrderBy(it => it)); 
    

    requires more than simple dot counting. We need to understand the sequence of operations and intermediate results.

  3. Extension Methods (like LINQ): How should we treat IEnumerable<T> extension methods? Technically, each call operates on the result of the previous one. RSCG_Demeter currently flags these as potential violations, promoting simpler data transformations, but this is a configurable aspect of LoD interpretation.
     empAll.Select(it => it.ID).Distinct().OrderBy(it => it)
    var ids = new List<int>(empAll.Select(it => it.ID).Distinct().OrderBy(it => it)); 
    

RSCG_Demeter in Action

This tool uses Roslyn’s powerful syntax and semantic analysis capabilities to look beyond simple dot counts and identify more nuanced potential LoD violations, while aiming to ignore common valid patterns like fluent builders.

Ready to enforce the Law of Demeter automatically?

Give RSCG_Demeter a try! Find the source code, usage instructions, and contribute here:
➡️ https://github.com/ignatandrei/RSCG_Demeter

Help your codebase avoid talking to strangers!

What I have learned by building .NET Stars -part 5 – always available data for a display website

What I have learned by building .NET Stars -part 5 – always available data for a display website

Dotnet Stars being a site just for displaying data, it does not require an API per se. Yes, for development purposes it needs a database and an API to display – but later – the data could be retrieved from local.

The first part is to write data in JSON files near to Blazor . But how to export by default this ?

And here ASPIRE thrives : I have made a console app to export data – and registered in ASPIRE with dependency of Blazor – and can see where Blazor folder is.

this is the extension

public static IResourceBuilder<TRes> AddPathToEnvironmment<TProject,TRes>(
        this IResourceBuilder<TRes> builder, TProject p, string name)
        where TProject : IProjectMetadata, new()
        where TRes : IResourceWithEnvironment            
{
    //var p = new TProject();        
    string pathPrj = p.ProjectPath;
    var fi = new FileInfo(pathPrj);
    string dirName = fi?.DirectoryName ?? "";
    var projectBuilder = builder
        .WithEnvironment(ctx=>
        {
            ctx.EnvironmentVariables[name] =dirName;
            ctx.EnvironmentVariables[$"{name}csproj"] = pathPrj;
        });

    return projectBuilder;
}

and this is how it is used

var exportToJson = builder.AddProject<Projects.StatsExport>("statsExport")
    .WithReference(ui)    
    .WithReference(db)
    .WaitFor(db)
    .AddPathToEnvironmment(new Projects.StatsBlazorUI(),"pathToWrite")
    ;

And the code that uses this

var pathToWrite = Environment.GetEnvironmentVariable("pathToWrite");
if (string.IsNullOrWhiteSpace(pathToWrite))
{
    Console.WriteLine("please add a path to write");
    return;
}

The second part is to get data from WebAPI , if available, and, if not, from JSON files.
And here the Roslyn Code Generator, https://github.com/ignatandrei/RSCG_CompositeProvider , it is useful .
We have 2 implementations of same interface ,

public interface IStatsData
{
   //other code
    IAsyncEnumerable<IProjectWithStars> GetProjectsWithStars();
}

And we have an implementation from WebAPI and another from JSON files

With the nugethttps://nuget.org/packages/RSCG_CompositeProviderwe can obtain data from the first that returns data.

builder.Services.AddKeyedScoped<IStatsData>("both", (sp, obj) =>
{
    var statsDataLocal = sp.GetRequiredKeyedService<IStatsData>("local_host");
    var statsDataAPI = sp.GetRequiredKeyedService<IStatsData>("statsconsole_host");
    StatsData_CP composite = new(statsDataAPI, statsDataLocal);
    composite.UseFirstTheLastOneThatWorks = true;
    return composite;
});

RSCG-Composite Provider – part 2 -execution

Imagine this: For every interface IA you create:

  1. Your composite provider implements IA seamlessly
  2. In the constructor, pass an array of IA providers
  3. Each method returns the first successful value from your array of providers
  4. For even better performance, use a boolean flag to optimize by reusing previous successes

The RSCG_CompositeProvider package doesnΓÇÖt just solve the obvious issuesΓÇöit handles the tricky ones too:

  • Exception Handling: What if one of your providers throws an error? No worries, it moves on to the next provider without skipping a beat.
  • Asynchronous Methods: Supports `Task` returns so you can handle async operations with ease.
  • Async Enumerables: Easily works with `IAsyncEnumerable` for streaming data in chunks.

I will let you think how to solve this – you can find my solution at https://github.com/ignatandrei/RSCG_CompositeProvider

And the best news? It’s already been tested: with over 20 tests covering every edge case, you can trust this library to handle your toughest challenges.

Get started with RSCG-Composite Provider, available via NuGet: http://nuget.org/packages/RSCG_CompositeProvider.

RSCG-Composite Provider – part 1 -idea

API Outage solved with local data

The problem that I try to solve is : How an UI can have data to show , even if the API from where it gathers data does not work ?

Imagine having a robust system that adaptively switches between retrieving data from multiple sources – including internal APIs, memory-based storage, or even JSON files near the UI. This flexibility makes all the difference when working with complex systems that may suddenly lose their external connectivity.

This could be solved with interfaces and a composite provider ( see http://msprogrammer.serviciipeweb.ro/2025/03/10/pattern-compositeprovider/ )

How It Works:

  • You have an interface defining how your UI fetches data.Easy peasy!
  • One implementation pulls data from the trusty API, ideally when it’s up and running smoothly.
  • Another implementation acts as a backup hero, pulling data from a local JSON file or even hard-coded values.

And the best part? The composite provider handles switching between these sources seamlessly. No more coding headaches – it just works!

Making It Even Easier: Roslyn Code Generator to the Rescue

Tired of writing boilerplate code for this pattern every time? Please give a chance to a new Roslyn Code Generator (https://www.nuget.org/packages/RSCG_CompositeProvider). It automatically generates the composite providers you need, cutting down on repetitive work and letting you focus on what really matters – building awesome apps!

What I have learned by building .NET Stars -part 4- Keyed Dependency Injection – constructor by keys

As I said before, I have constructed first the interfaces. Then I have a null object pattern for each interface, constructed automatically with a Roslyn Code Generator , https://www.nuget.org/packages/rscg_Interface_to_null_object .

And now, what I want is to have keyed DI for each new implementation -with a twist: It’s like a game of matching keys when another class is injected, the first one gets chosen if it has the exact same key!

Let me give an illustrative example :

Imagine interfaces IA and IB, with Class_A_Null and Class_B_Null as their null object counterparts. These are injected by default into our DI container

We have class class_A_Data1 that implements IA.
It is injected with key “Data1”

We have class B_Data1 ( implements IB ) that has a constructor that has a parameter IA .
It is injected with key “Data1”

We have class B_Data2 ( implements IB ) that has a constructor that has a parameter IA .
It is injected with key “Data2”

So every time we construct an IB it requires it’s companion , IA .
 
Now  let’s say I want DI to construct B_Data1 and B_Data2

When want to construct B_Data1 , it sees that has a Key “Data1” . See that he needs also an IA –
DI can choose between default implementation “Class_A_Null” ( without key ) and “class_A_Data1” with the key “Data1”
Because it has the same key (“Data1” ) as the class that must be implemented (“B_Data1” ) chooses class_A_Data1

When want to construct B_Data2 , it sees that has a Key “Data2” . See that he needs also an IA.
DI can choose between default implementation “Class_A_Null” ( without key ) and “class_A_Data1” with the key “Data1”
Because the class to be constructed has the key “Data2”, the “class_A_Data1” with the key “Data1” is eliminated.
So it constructs B_Data2 with the default implementation , “Class_A_Null” ( without key )

Why this constraint ? Because I started with null implementation, and add one by one the implementation. 

The WebAPI  works every time ( the null implementation does nothing , so it works ).
And slowly but surely we add to the DI key another implementation and we are sure that works every time .

This innovative use of keyed dependency injection, particularly starting with null object patterns, allows for iterative development without breaking existing functionalities. As each new implementation is added, the system remains stable and functional, gradually enhancing its capabilities with each iteration.

Feel free to experiment with these ideas and adapt them to your own projects. Start by trying out the rscg_Interface_to_null_object package, and see how it can transform your development workflow

You can see at https://ignatandrei.github.io/dotnetstats/ and the source code at http://github.com/ignatandrei/dotnetstats/

The code for this special DI is at https://github.com/ignatandrei/dotnetstats/blob/main/src/Stats/StatsConsole/KeyedServiceCollection.cs ( too long to put here )

What I have learned by building .NET Stars -part 3- Aspire

I have that same idea to build a project : is what is called today a Modular Monolith – and it is very old in .NET world as can be implemented as a .sln solution.
For those unfamiliar, a Modular Monolith allows you to break down your application into independent modules (think database access, data flow logic, a sleek UI), yet keep them tightly integrated within a single, cohesive solution.

It’s how all these components can work together seamlessly. This idea resonated with me when I started thinking about my project. So, let me break it down:

Interfaces for Data/Flow These lay the foundation, ensuring that data and operations flow smoothly between different parts of the project.
  
– A database stores all the necessary information, serving as the backbone of my application.

– Then there’s the WebAPI, which acts like a messenger, transferring data between the database and the users’ interfaces.

– And finally, for the User Interface, I’ve chosen Blazor. It brings together code, design, and interactivity i

Aspire is a game-changer in the .NET world, offering a simple yet powerful way to coordinate multiple projects. By starting an app host with all the projects intertwined, it simplifies the process of building complex applications.  More, Aspire let me coordinate another project to save the data on Blazor – but this will be discussed in another blog post later .

I have had just 2 modifications to make it work flawlessly :

1. Blazor

  To know the address of the WebAPI to obtain the data ( once published, Blazor will be near the WebAPI in the wwwroot, but until then it needs the adress )

Blazor can have the configuration stored in a appsettings.json in the wwwroot – but how to write ? I developed an extension for ASPIRE in order to write the data

2. Database

In order to have the database with data , I need to write scripts for create table / insert the data.

This code shows how

var paramPass = builder.AddParameter("password", "P@ssw0rd");

var sqlserver = builder.AddSqlServer("sqlserver",paramPass,1433)
    //.WithArgs("pwd","&amp;","ls")
    // Mount the init scripts directory into the container.
    .WithBindMount("./sqlserverconfig", "/usr/config")
    // Mount the SQL scripts directory into the container so that the init scripts run.
    .WithBindMount("../../Scripts/data/sqlserver", "/docker-entrypoint-initdb.d")
    // Run the custom entrypoint script on startup.
    .WithEntrypoint("/usr/config/entrypoint.sh")
    // Configure the container to store data in a volume so that it persists across instances.
    .WithDataVolume() 
    // Keep the container running between app host sessions.
    .WithLifetime(ContainerLifetime.Persistent)

    ;
var db= sqlserver.AddDatabase("DotNetStats");

Also , I have an .gitattributes that contains

* text=auto
*.sh text eol=lf
*.mod text eol=lf
*.sum text eol=lf

in order for the .sh files to maintain linux ending.

You can see at https://ignatandrei.github.io/dotnetstats/ and the source code at http://github.com/ignatandrei/dotnetstats/

What I have learned by building .NET Stars -part 2- interfaces

In my latest project, building a robust system using interfaces has been both a fascinating challenge and an enlightening experience. As I embarked on this journey, it quickly became evident how immensely powerful and transformative thinking in terms of interfaces can be.

From the outset, interfacing brought clarity to several core aspects of the application:

  • Data Management: Interfaces provided a transparent view of data structures and their usage within the app.
  • Data Flow: They illuminated how data was accessed and exchanged, simplifying what seemed complex at first glance.
  • Application Flow: Interfaces helped chart out the application’s workflow, providing insights even in its dormant state.

While initially daunting, embracing interfaces has offered immense benefits almost immediately. A notable revelation was how adopting the NullObjectPattern through an interface (you can explore this further at rscg_Interface_to_null_object) enabled me to navigate and visualize my application’s flow effortlessly – a crucial step even when it performs no operations.

One of the quicker wins was leveraging interfaces to support multiple data sources. This flexibility meant that I could seamlessly pull project details from diverse platforms like the .NET Foundation, GitHub repositories such as quozd/awesome-dotnet and thangchung/awesome-dotnet-core by implementing a common interface, IStatsData, and then efficiently saving this consolidated data with a single implementation of IProjectsData.

Our interface designs have opened up a world of possibilities for flexibility and reusability. Consider these interfaces :

[ToNullObject]
public interface IStatsData
{
    //other code
IAsyncEnumerable<IProject> RefreshProjects();
}

[ToNullObject]
public interface IProjectsData
{
    IAsyncEnumerable<IProject> GetProjectsAsync();
    Task<bool> SaveProjects(IProject[] projects);

}

 

With these interfaces, we have the freedom to implement various strategies for acquiring projects from diverse sources such as DotNetFoundation, awesome-dotnet, and awesome-dotnet-core. Moreover, we can centralize the project storage logic with just one implementation of IProjectsData.

The result is a dynamic, versatile application that not only offers rich functionality but also encourages continuous improvement.

Eager to see the fruits of this approach? You can dive into our project live at https://ignatandrei.github.io/dotnetstats/. Additionally, the source code is available for all to explore at http://github.com/ignatandrei/dotnetstats/.

Andrei Ignat weekly software news(mostly .NET)

* indicates required

Please select all the ways you would like to hear from me:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.