bchavez / Bogus

:card_index: A simple fake data generator for C#, F#, and VB.NET. Based on and ported from the famed faker.js.
Other
8.86k stars 505 forks source link
bogus c-sharp csharp data data-access-layer data-generator database dotnet fake faker generator poco seed test-data

Downloads Build status Twitter Chat

Bogus for .NET: C#, F#, and VB.NET

Project Description

Hello. I'm your host Brian Chavez (twitter). Bogus is a simple fake data generator for .NET languages like C#, F# and VB.NET. Bogus is fundamentally a C# port of faker.js and inspired by FluentValidation's syntax sugar.

Bogus will help you load databases, UI and apps with fake data for your testing needs. If you like Bogus star :star: the repository and show your friends! :smile: If you find Bogus useful consider supporting the project by purchasing a Bogus Premium license that gives you extra Bogus superpowers! :dizzy: :muscle: You can also sponsor the project here! :moneybag: :dollar:

Download & Install

Nuget Package Bogus

Install-Package Bogus

Minimum Requirements: .NET Standard 1.3 or .NET Standard 2.0 or .NET Framework 4.0.

Projects That Use Bogus
Featured In
Blog Posts
The Crypto Tip Jar!

Usage

The Great C# Example

public enum Gender
{
    Male,
    Female
}

//Set the randomizer seed if you wish to generate repeatable data sets.
Randomizer.Seed = new Random(8675309);

var fruit = new[] { "apple", "banana", "orange", "strawberry", "kiwi" };

var orderIds = 0;
var testOrders = new Faker<Order>()
    //Ensure all properties have rules. By default, StrictMode is false
    //Set a global policy by using Faker.DefaultStrictMode
    .StrictMode(true)
    //OrderId is deterministic
    .RuleFor(o => o.OrderId, f => orderIds++)
    //Pick some fruit from a basket
    .RuleFor(o => o.Item, f => f.PickRandom(fruit))
    //A random quantity from 1 to 10
    .RuleFor(o => o.Quantity, f => f.Random.Number(1, 10))
    //A nullable int? with 80% probability of being null.
    //The .OrNull extension is in the Bogus.Extensions namespace.
    .RuleFor(o => o.LotNumber, f => f.Random.Int(0, 100).OrNull(f, .8f));

var userIds = 0;
var testUsers = new Faker<User>()
    //Optional: Call for objects that have complex initialization
    .CustomInstantiator(f => new User(userIds++, f.Random.Replace("###-##-####")))

    //Use an enum outside scope.
    .RuleFor(u => u.Gender, f => f.PickRandom<Gender>())

    //Basic rules using built-in generators
    .RuleFor(u => u.FirstName, (f, u) => f.Name.FirstName(u.Gender))
    .RuleFor(u => u.LastName, (f, u) => f.Name.LastName(u.Gender))
    .RuleFor(u => u.Avatar, f => f.Internet.Avatar())
    .RuleFor(u => u.UserName, (f, u) => f.Internet.UserName(u.FirstName, u.LastName))
    .RuleFor(u => u.Email, (f, u) => f.Internet.Email(u.FirstName, u.LastName))
    .RuleFor(u => u.SomethingUnique, f => $"Value {f.UniqueIndex}")

    //Use a method outside scope.
    .RuleFor(u => u.CartId, f => Guid.NewGuid())
    //Compound property with context, use the first/last name properties
    .RuleFor(u => u.FullName, (f, u) => u.FirstName + " " + u.LastName)
    //And composability of a complex collection.
    .RuleFor(u => u.Orders, f => testOrders.Generate(3).ToList())
    //Optional: After all rules are applied finish with the following action
    .FinishWith((f, u) =>
        {
            Console.WriteLine("User Created! Id={0}", u.Id);
        });

var user = testUsers.Generate();
Console.WriteLine(user.DumpAsJson());

/* OUTPUT:
User Created! Id=0
 *
{
  "Id": 0,
  "FirstName": "Audrey",
  "LastName": "Spencer",
  "FullName": "Audrey Spencer",
  "UserName": "Audrey_Spencer72",
  "Email": "Audrey82@gmail.com",
  "Avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/itstotallyamy/128.jpg",
  "CartId": "863f9462-5b88-471f-b833-991d68db8c93",
  "SSN": "923-88-4231",
  "Gender": 0,
  "Orders": [
    {
      "OrderId": 0,
      "Item": "orange",
      "Quantity": 8
    },
    {
      "OrderId": 1,
      "Item": "banana",
      "Quantity": 2
    },
    {
      "OrderId": 2,
      "Item": "kiwi",
      "Quantity": 9
    }
  ]
} */

More Examples!

Language Description
C# Full working example of 'The Great C# Example'
C# Using Bogus and EF Core to a seed database
C# Extending Bogus with custom APIs and data
F# Using Bogus with F#
VB.NET Using Bogus with VB.NET

Locales

Since we're a port of faker.js, we support a whole bunch of different locales. Here's an example in Korean:

[Test]
public void With_Korean_Locale()
{
    var lorem = new Bogus.DataSets.Lorem(locale: "ko");
    Console.WriteLine(lorem.Sentence(5));
}

/* 국가는 무상으로 행위로 의무를 구성하지 신체의 처벌받지 예술가의 경우와 */

Bogus supports the following locales:

Locale Code Language Locale Code Language
af_ZA Afrikaans fr_CH French (Switzerland)
ar Arabic ge Georgian
az Azerbaijani hr Hrvatski
cz Czech id_ID Indonesia
de German it Italian
de_AT German (Austria) ja Japanese
de_CH German (Switzerland) ko Korean
el Greek lv Latvian
en English nb_NO Norwegian
en_AU English (Australia) ne Nepalese
en_AU_ocker English (Australia Ocker) nl Dutch
en_BORK English (Bork) nl_BE Dutch (Belgium)
en_CA English (Canada) pl Polish
en_GB English (Great Britain) pt_BR Portuguese (Brazil)
en_IE English (Ireland) pt_PT Portuguese (Portugal)
en_IND English (India) ro Romanian
en_NG Nigeria (English) ru Russian
en_US English (United States) sk Slovakian
en_ZA English (South Africa) sv Swedish
es Spanish tr Turkish
es_MX Spanish (Mexico) uk Ukrainian
fa Farsi vi Vietnamese
fi Finnish zh_CN Chinese
fr French zh_TW Chinese (Taiwan)
fr_CA French (Canada) zu_ZA Zulu (South Africa)

Note: Some locales may not have a complete data set. For example, zh_CN does not have a lorem data set, but ko has a lorem data set. Bogus will default to en if a locale-specific data set is not found. To further illustrate the previous example, the missing zh_CN:lorem data set will default to the en:lorem data set.

If you'd like to help contribute new locales or update existing ones please see our Creating Locales wiki page for more info.

Without Fluent Syntax

You can use Bogus without a fluent setup. The examples below highlight three alternative ways to use Bogus without a fluent syntax setup.

Using the Faker facade:

public void Using_The_Faker_Facade()
{
   var faker = new Faker("en");
   var o = new Order()
       {
           OrderId = faker.Random.Number(1, 100),
           Item = faker.Lorem.Sentence(),
           Quantity = faker.Random.Number(1, 10)
       };
   o.Dump();
}

Using DataSets directly:

public void Using_DataSets_Directly()
{
   var random = new Bogus.Randomizer();
   var lorem = new Bogus.DataSets.Lorem("en");
   var o = new Order()
       {
           OrderId = random.Number(1, 100),
           Item = lorem.Sentence(),
           Quantity = random.Number(1, 10)
       };
   o.Dump();
}

Using Faker<T> inheritance:

public class OrderFaker : Faker<Order> {
   public OrderFaker() {
      RuleFor(o => o.OrderId, f => f.Random.Number(1, 100));
      RuleFor(o => o.Item, f => f.Lorem.Sentence());
      RuleFor(o => o.Quantity, f => f.Random.Number(1, 10));
   }
}

public void Using_FakerT_Inheritance()
{
   var orderFaker = new OrderFaker();
   var o = orderFaker.Generate();
   o.Dump();
}

In the examples above, all three alternative styles of using Bogus produce the same Order result:

{
  "OrderId": 61,
  "Item": "vel est ipsa",
  "Quantity": 7
}

Bogus API Support

API Extension Methods

Amazing Community Extensions

Bogus Premium Extensions!

Bogus Premium [Purchase Now!] by @bchavez
You can help support the Bogus open source project by purchasing a Bogus Premium license! With an active premium license you'll be supporting this cool open-source project. You'll also gain new superpowers that extended Bogus with new features and exclusive data sets! Check 'em out below!


Helper Methods

The features shown below come standard with the Bogus NuGet package.

Person

If you want to generate a Person with context relevant properties like an email that looks like it belongs to someone with the same first/last name, create a person!

[Test]
public void Create_Context_Related_Person()
{
    var person = new Bogus.Person();

    person.Dump();
}

/* OUTPUT:
{
  "FirstName": "Lee",
  "LastName": "Brown",
  "UserName": "Lee_Brown3",
  "Avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/ccinojasso1/128.jpg",
  "Email": "Lee_Brown369@yahoo.com",
  "DateOfBirth": "1984-01-16T21:31:27.87666",
  "Address": {
    "Street": "2552 Bernard Rapid",
    "Suite": "Suite 199",
    "City": "New Haskell side",
    "ZipCode": "78425-0411",
    "Geo": {
      "Lat": -35.8154,
      "Lng": -140.2044
    }
  },
  "Phone": "1-500-790-8836 x5069",
  "Website": "javier.biz",
  "Company": {
    "Name": "Kuphal and Sons",
    "CatchPhrase": "Organic even-keeled monitoring",
    "Bs": "open-source brand e-business"
  }
} */

Replace

Replace a formatted string with random numbers #, letters ?, or * random number or letter:

[Test]
public void Create_an_SSN()
{
    var ssn = new Bogus.Randomizer().Replace("###-##-####");
    ssn.Dump();

    var code = new Randomizer().Replace("##? ??? ####");
    code.Dump();

    var serial = new Randomizer().Replace("**-****");
    serial.Dump();
}
/* OUTPUT:
"618-19-3064"
"39E SPC 0790"
"L3-J9N5"
*/

Parse Handlebars

You can also parse strings in the following format:

[Test]
public void Handlebar()
{
    var faker = new Faker();
    var randomName = faker.Parse("{{name.lastName}}, {{name.firstName}} {{name.suffix}}");
    randomName.Dump();
}

/* OUTPUT:
"Roob, Michale PhD"
*/

The name of a dataset is determined using DataCategory attribute or class name otherwise. (i.e PhoneNumber dataset in handlebars expression should be named as phone_number)

You can pass parameters to methods using braces:

[Test]
public void HandlebarWithParameters()
{
    var faker = new Faker();
    var randomName = faker.Parse("{{name.firstname(Female)}}, {{name.firstname(Male)}}");
    randomName.Dump();
}

/* OUTPUT:
"Lindsay, Jonathan"
*/

Implicit and Explicit Type Conversion

You can also use implicit type conversion to make your code look cleaner without having to explicitly call Faker<T>.Generate().

var orderFaker = new Faker<Order>()
                     .RuleFor(o => o.OrderId, f => f.IndexVariable++)
                     .RuleFor(o => o.Item, f => f.Commerce.Product())
                     .RuleFor(o => o.Quantity, f => f.Random.Number(1,3));

Order testOrder1 = orderFaker;
Order testOrder2 = orderFaker;
testOrder1.Dump();
testOrder2.Dump();

/* OUTPUT:
{
  "OrderId": 0,
  "Item": "Computer",
  "Quantity": 2
}
{
  "OrderId": 1,
  "Item": "Tuna",
  "Quantity": 3
}
*/

//Explicit works too!
var anotherOrder = (Order)orderFaker;

Bulk Rules

Sometimes writing .RuleFor(x => x.Prop, ...) can get repetitive, use the .Rules((f, t) => {...}) shortcut to specify rules in bulk as shown below:

public void create_rules_for_an_object_the_easy_way()
{
    var faker = new Faker<Order>()
        .StrictMode(false)
        .Rules((f, o) =>
            {
                o.Quantity = f.Random.Number(1, 4);
                o.Item = f.Commerce.Product();
                o.OrderId = 25;
            });
    Order o = faker.Generate();
}

Note: When using the bulk .Rules(...) action, StrictMode cannot be set to true since individual properties of type T cannot be independently checked to ensure each property has a rule.

Using Enumerable.Range() and LINQ

The Enumerable.Range() and LINQ are a great supplement when creating data with Bogus. Here's how to generate a simple list of email addresses:

var faker = new Faker("en");

var emailList = Enumerable.Range(1, 5)
      .Select(_ => faker.Internet.Email())
      .ToList();

//OUTPUT:
Gustave83@hotmail.com    
Evie33@gmail.com 
Abby_Wilkinson@yahoo.com 
Cecilia.Hahn@yahoo.com   
Jasen.Waelchi85@gmail.com     

Advanced Topics, Guidance, and Best Practices

Determinism

Determinism is a first class concept in Bogus. Bogus goes to great lengths so developers can generate the same sequence of data over multiple program executions. Bogus has two strategies of setting up deterministic behavior:

  1. Global Seed determinism through the Randomizer.Seed global static property.
    Pros: Easy to get deterministic data setup quickly.
    Cons: Code changes can impact other data values. Not so good for unit tests.

  2. Local Seed determinism through instance properties and methods. Specifically,

    • The Faker<T>.UseSeed(int) method.
    • The .Random property on the Faker facade and DataSets.

    Pros: Code changes can be isolated with minimal impact on determinism. Good for unit tests.
    Cons: Requires some forethought in design.

When Local Seed determinism is used to set a seed value, the global static source of randomness is ignored. This has some interesting implications as described below.

Using Global Seed determinism

The easiest way to get deterministic data values over multiple executions of a program is to set the Randomizer.Seed property as demonstrated below:

Randomizer.Seed = new Random(1338);
var orderIds = 0;
var orderFaker = new Faker<Order>()
    .RuleFor(o => o.OrderId, f => orderIds++)
    .RuleFor(o => o.Item, f => f.Commerce.Product())
    .RuleFor(o => o.Quantity, f => f.Random.Number(1, 5));

orderFaker.Generate(5).Dump();
OrderId Item Quantity
0 Fish 3
1 Chair 1
2 Gloves 5
3 Shirt 4
4 Hat 4

Re-running the code above with 1338 as a global static seed value will produce the same table of data over and over again.

Next, add a new Description property to the Order class along with a new .RuleFor(o => o.Description, ..) rule and see the data changes:

Randomizer.Seed = new Random(1338);
var orderIds = 0;
var orderFaker = new Faker<Order>()
    .RuleFor(o => o.OrderId, f => orderIds++)
    .RuleFor(o => o.Item, f => f.Commerce.Product())
    .RuleFor(o => o.Description, f => f.Commerce.ProductAdjective()) //New Rule
    .RuleFor(o => o.Quantity, f => f.Random.Number(1, 5));

orderFaker.Generate(5).Dump();
OrderId Item Description Quantity
0 Fish Fantastic :triangular_flag_on_post: 1
1 :triangular_flag_on_post: Keyboard :triangular_flag_on_post: Gorgeous :triangular_flag_on_post: 5
2 :triangular_flag_on_post: Shirt :triangular_flag_on_post: Handcrafted :triangular_flag_on_post: 3
3 :triangular_flag_on_post: Tuna :triangular_flag_on_post: Small :triangular_flag_on_post: 1
4 :triangular_flag_on_post: Table :triangular_flag_on_post: Awesome :triangular_flag_on_post: 3

A couple of observations:

In fact, every data value with a :triangular_flag_on_post: icon has changed. This is due to the newly added property which has the effect of shifting the entire global static pseudo-random sequence off by +1. This rippling effect can be a problem if unit tests are expecting data values to remain the same. The following section below shows how we can improve the situation.

Using Local Seed determinism

Making use of the Faker<T>.UseSeed(int) method can help limit the impact of POCO schema changes on deterministic data values that span across an entire run. Consider the following code that uses a seed value for each instance of a POCO object:

var orderIds = 0;
var orderFaker = new Faker<Order>()
    .RuleFor(o => o.OrderId, f => orderIds++)
    .RuleFor(o => o.Item, f => f.Commerce.Product())
    .RuleFor(o => o.Quantity, f => f.Random.Number(1, 5));

Order SeededOrder(int seed){
   return orderFaker.UseSeed(seed).Generate();
}

var orders = Enumerable.Range(1, 5)
   .Select(SeededOrder)
   .ToList();

orders.Dump();
OrderId Item Quantity
0 Bike 1
1 Cheese 3
2 Gloves 4
3 Bacon 5
4 Pants 2

Next, adding the Description property to the Order class and examining the output:

var orderIds = 0;
var orderFaker = new Faker<Order>()
    .RuleFor(o => o.OrderId, f => orderIds++)
    .RuleFor(o => o.Item, f => f.Commerce.Product())
    .RuleFor(o => o.Description, f => f.Commerce.ProductAdjective()) //New Rule
    .RuleFor(o => o.Quantity, f => f.Random.Number(1, 5));

Order SeededOrder(int seed){
   return orderFaker.UseSeed(seed).Generate();
}

var orders = Enumerable.Range(1,5)
   .Select(SeededOrder)
   .ToList();

orders.Dump();
OrderId Item Description Quantity
0 Bike Ergonomic :triangular_flag_on_post: 3
1 Cheese Fantastic :triangular_flag_on_post: 1
2 Gloves Handcrafted :triangular_flag_on_post: 5
3 Bacon Tasty :triangular_flag_on_post: 3
4 Pants Gorgeous :triangular_flag_on_post: 2

Progress! This time only the Quantity data values with the :triangular_flag_on_post: icon have changed. The Item column remained the same before and after the new addition of the Description property.

We can further prevent the Quantity data values from changing by moving the RuleFor(o => o.Description,...) rule line to the end of the Faker<Order> declaration as shown below:

var orderIds = 0;
var orderFaker = new Faker<Order>()
    .RuleFor(o => o.OrderId, f => orderIds++)
    .RuleFor(o => o.Item, f => f.Commerce.Product())
    .RuleFor(o => o.Quantity, f => f.Random.Number(1, 5))
    .RuleFor(o => o.Description, f => f.Commerce.ProductAdjective()); //New Rule

Order MakeOrder(int seed){
   return orderFaker.UseSeed(seed).Generate();
}

var orders = Enumerable.Range(1,5)
   .Select(MakeOrder)
   .ToList();

orders.Dump();
OrderId Item Quantity Description
0 Bike 1 Practical
1 Cheese 3 Rustic
2 Gloves 4 Refined
3 Bacon 5 Awesome
4 Pants 2 Gorgeous

Much success! :100: :tada: The Item and Quantity values remain unchanged! The new Description property is added to the POCO object without any impact to other deterministic data values.

As a best practice, to achieve maximum deterministic behavior and unit test robustness with Bogus:


The Faker facade and individual DataSets can also be prepared to use local seeds as well. The following shows how to set up the Faker facade and DataSets to use local seeds:

var faker = new Faker("en")
                {
                   Random = new Randomizer(1338)
                };
var lorem = new Bogus.DataSets.Lorem("en"){
                   Random = new Randomizer(1338)
                };
faker.Lorem.Word().Dump();
lorem.Word().Dump();

//OUTPUT:
minus
minus

The .Random property can be set multiple times without any ill effects.

Versioning can effect determinism

Updating to new versions of Bogus on NuGet can effect determinism too. For example, when Bogus updates locales from faker.js or issues bug fixes, sometimes deterministic sequences can change. Changes to deterministic outputs are usually highlighted in the release notes. Changes to deterministic outputs is also considered a breaking change. Bogus generally follows semantic versioning rules. For example:

Version Description
Bogus v25.0.1 Initial version.
Bogus v25.0.2 No change to deterministic outputs or breaking changes. Possible bug fixes & improvements.
Bogus v26.0.1 Deterministic outputs may have changed or may include other breaking changes.

As a general rule of thumb,

Deterministic Dates and Times

Bogus can generate deterministic dates and times. However, generating deterministic dates and times requires the following:

  1. Setting up a seed value.
  2. Setting up a time reference for your Faker object instance.

The following code shows how to setup deterministic dates and times:

// Faker[T]: Set a local seed and a time reference
var fakerT = new Faker<Order>()
                 .UseSeed(1338)
                 .UseDateTimeReference(DateTime.Parse("1/1/1980"))
                 .RuleFor(o => o.SoonValue,   f => f.Date.Soon())
                 .RuleFor(o => o.RecentValue, f => f.Date.Recent());
fakerT.Generate().Dump();
//  { "SoonValue":   "1980-01-01T17:33:05",
//    "RecentValue": "1979-12-31T14:07:31" }

// Faker: Set a local seed and a time reference
var faker = new Faker
   {
      Random = new Randomizer(1338),
      DateTimeReference = DateTime.Parse("1/1/1980")
   };
faker.Date.Soon();   // "1980-01-01T17:33:05"
faker.Date.Recent(); // "1979-12-31T14:07:31"

With a time reference set and a seed, dates and times should be deterministic across multiple runs of a program.

F# and VB.NET Examples

The Fabulous F# Examples

type Customer = { FirstName : string
                  LastName : string
                  Age : int
                  Title : string }

//The faker facade
let f = Faker();

let generator() = 
   { FirstName = f.Name.FirstName()
     LastName  = f.Name.LastName()
     Age       = f.Random.Number(18,60)
     Title     = f.Name.JobTitle() }

generator() |> Dump |> ignore

(* OUTPUT:
  FirstName = "Russell"
  LastName = "Nader"
  Age = 34
  Title = "Senior Web Officer"
*)
type Customer = { FirstName : string
                  LastName : string
                  Age : int
                  Title : string }

let customerFaker =
    Bogus
        .Faker<Customer>()
        .CustomInstantiator(fun f ->
             { FirstName = f.Name.FirstName()
               LastName  = f.Name.LastName()
               Age       = f.Random.Number(18,60)
               Title     = f.Name.JobTitle() })

customerFaker.Generate() |> Dump |> ignore

(* OUTPUT:
  FirstName = "Sasha"
  LastName = "Roberts"
  Age = 20;
  Title = "Internal Security Specialist"
*)
open Bogus
type Customer() =
  member val FirstName = "" with get, set
  member val LastName = "" with get, set
  member val Age = 0 with get, set
  member val Title = "" with get, set

let faker = 
        Faker<Customer>()
          //Make a rule for each property
          .RuleFor( (fun c -> c.FirstName), fun (f:Faker) -> f.Name.FirstName() )
          .RuleFor( (fun c -> c.LastName), fun (f:Faker) -> f.Name.LastName() )

          //Or, alternatively, in bulk with .Rules()
          .Rules( fun f c -> 
                    c.Age <- f.Random.Int(18,35) 
                    c.Title <- f.Name.JobTitle() )

faker.Generate() |> Dump |> ignore

(* OUTPUT:
  FirstName: Jarrell
  LastName: Tremblay
  Age: 32
  Title: Senior Web Designer
*)

The Very Basic VB.NET Example

Imports Bogus

Public Class Customer
    Public Property FirstName() As String
    Public Property LastName() As String
    Public Property Age() As Integer
    Public Property Title() As String
End Class

Sub Main
    Dim faker As New Faker(Of Customer)

    '-- Make a rule for each property
    faker.RuleFor( Function(c) c.FirstName, Function(f) f.Name.FirstName) _
         .RuleFor( Function(c) c.LastName, Function(f) f.Name.LastName) _
         _
         .Rules( Sub(f, c)   '-- Or, alternatively, in bulk with .Rules() 
                   c.Age = f.Random.Int(18,35) 
                   c.Title = f.Name.JobTitle()
                 End Sub )

    faker.Generate.Dump
End Sub

' OUTPUT:
' FirstName: Jeremie 
' LastName: Mills 
' Age: 32 
' Title: Quality Supervisor 

Building From Source

The following section is only useful for people looking to contribute to Bogus or make custom modifications to Bogus. This section includes information about building Bogus from source code and is not required to operate or run Bogus in .NET applications.

The minimum requirements to build Bogus from source code are as follows:

Build Instructions

The following folders will be created depending on the build task executed:

Build Environment Variables

Rebundling Locales

Re-bundling the latest locale data from faker.js requires the following software installed:

Steps to re-bundle locale data from faker.js:

  1. git clone https://github.com/bchavez/Bogus.git
  2. cd Bogus
  3. git submodule init
  4. git submodule update
  5. Ensure NodeJS and gulp are properly installed.
  6. cd Source\Builder
  7. npm install to install required dev dependencies.
  8. npx gulp importLocales to regenerate locales in Source\Bogus\data.
  9. Finally, run build.cmd.

License

Sponsors

A special thank you to the companies that have sponsored and helped with the development of Bogus in big ways.

Date Company
2024 - October Amazon AWS .NET FOSS Fund
2022 - June GitHub

Contributors

Created by Brian Chavez.

A big thanks to GitHub and all contributors: