diff --git a/CSharpFunctionalExtensions.Examples/LinqQuery/ResultQueryExamples.cs b/CSharpFunctionalExtensions.Examples/LinqQuery/ResultQueryExamples.cs
new file mode 100644
index 00000000..7ec9998f
--- /dev/null
+++ b/CSharpFunctionalExtensions.Examples/LinqQuery/ResultQueryExamples.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions.Examples.LinqQuery
+{
+ ///
+ /// Demonstrates using LINQ query syntax (sugar) as an alternative to
+ /// fluent method chaining to compose operations in a monadic style,
+ /// analogous to F# computation expressions or Haskell's do-notation.
+ ///
+ public class ResultQueryExamples
+ {
+ public async Task Demo()
+ {
+ // Using the LINQ query syntax:
+ var validCustomer =
+ from name in CustomerName.Create("jsmith")
+ from email in Email.Create("jsmith@example.com")
+ select new Customer(name, email);
+
+ // Equivalent to:
+
+ var validCustomer_ = CustomerName
+ .Create("jsmith")
+ .Bind(name =>
+ Email.Create("jsmith@example.com").Map(email => new Customer(name, email))
+ );
+
+ // Success(Customer(Name: jsmith, Email: jsmith@example.com))
+ Console.WriteLine(validCustomer);
+
+ var invalidCustomer =
+ from name in CustomerName.Create("jsmith")
+ from email in Email.Create("no email")
+ select new Customer(name, email);
+
+ // Failure(E-mail is invalid)
+ Console.WriteLine(invalidCustomer);
+
+ //------------------------------------------------------------------------------
+ // Also works with async methods.
+
+ var billing = await (
+ from customer in CustomerRepository.GetByIdAsync(1) // Task>
+ from billingInfo in PaymentGateway.ChargeCustomerAsync(customer, 1_000) // Task>
+ select billingInfo
+ );
+
+ // Equivalent to:
+ var billing_ = await CustomerRepository
+ .GetByIdAsync(1)
+ .Bind(customer => PaymentGateway.ChargeCustomerAsync(customer, 1_000));
+
+ // Success(BillingInfo(Customer: jsmith@example.com, ChargeAmount: 1000))
+ Console.WriteLine(billing);
+
+ var failedBilling = await (
+ from customer in CustomerRepository.GetByIdAsync(1)
+ from billingInfo in PaymentGateway.ChargeCustomerAsync(customer, 5_000_000)
+ select billingInfo
+ );
+
+ // Failure(Insufficient balance.)
+ Console.WriteLine(failedBilling);
+ }
+ }
+
+ public class Email
+ {
+ private readonly string _value;
+
+ private Email(string value)
+ {
+ _value = value;
+ }
+
+ public override string ToString()
+ {
+ return _value;
+ }
+
+ public static Result Create(string email)
+ {
+ if (string.IsNullOrWhiteSpace(email))
+ return Result.Failure("E-mail can't be empty");
+
+ if (email.Length > 100)
+ return Result.Failure("E-mail is too long");
+
+ if (!Regex.IsMatch(email, @"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$"))
+ return Result.Failure("E-mail is invalid");
+
+ return Result.Success(new Email(email));
+ }
+ }
+
+ public class CustomerName
+ {
+ private readonly string _value;
+
+ private CustomerName(string value)
+ {
+ _value = value;
+ }
+
+ public override string ToString()
+ {
+ return _value;
+ }
+
+ public static Result Create(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return Result.Failure("Name can't be empty");
+
+ if (name.Length > 50)
+ return Result.Failure("Name is too long");
+
+ return Result.Success(new CustomerName(name));
+ }
+ }
+
+ public class Customer
+ {
+ public CustomerName Name { get; private set; }
+ public Email Email { get; private set; }
+
+ public Customer(CustomerName name, Email email)
+ {
+ if (name == null)
+ throw new ArgumentNullException("name");
+ if (email == null)
+ throw new ArgumentNullException("email");
+
+ Name = name;
+ Email = email;
+ }
+
+ public override string ToString()
+ {
+ return $"{nameof(Customer)}({nameof(Name)}: {Name}, {nameof(Email)}: {Email})";
+ }
+ }
+
+ public class CustomerRepository
+ {
+ public static async Task> GetByIdAsync(int id)
+ {
+ var customer =
+ from email in Email.Create("jsmith@example.com")
+ from name in CustomerName.Create("jsmith")
+ select new Customer(name, email);
+
+ return customer;
+ }
+ }
+
+ public class BillingInfo
+ {
+ public Customer Customer { get; set; }
+ public decimal ChargeAmount { get; set; }
+
+ public override string ToString()
+ {
+ return $"{nameof(BillingInfo)}({nameof(Customer)}: {Customer.Email}, {nameof(ChargeAmount)}: {ChargeAmount})";
+ }
+ }
+
+ public class PaymentGateway
+ {
+ public static async Task> ChargeCustomerAsync(
+ Customer customer,
+ decimal chargeAmount
+ )
+ {
+ if (chargeAmount > 1_000_000)
+ return Result.Failure("Insufficient balance");
+
+ return Result.Success(
+ new BillingInfo { Customer = customer, ChargeAmount = chargeAmount }
+ );
+ }
+ }
+}
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/Select.Task.cs b/CSharpFunctionalExtensions/Maybe/Extensions/Select.Task.cs
new file mode 100644
index 00000000..a3a8eba2
--- /dev/null
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/Select.Task.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions
+{
+ public static partial class MaybeExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Map method.
+ ///
+ public static Task> Select(this Task> maybe, Func selector)
+ {
+ return maybe.Map(selector);
+ }
+ }
+}
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/Select.ValueTask.cs b/CSharpFunctionalExtensions/Maybe/Extensions/Select.ValueTask.cs
new file mode 100644
index 00000000..47f111bc
--- /dev/null
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/Select.ValueTask.cs
@@ -0,0 +1,18 @@
+#if NET5_0_OR_GREATER
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions.ValueTasks
+{
+ public static partial class MaybeExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Map method.
+ ///
+ public static ValueTask> Select(in this ValueTask> maybe, Func selector)
+ {
+ return maybe.Map(selector);
+ }
+ }
+}
+#endif
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Left.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Left.cs
new file mode 100644
index 00000000..7910e4b0
--- /dev/null
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Left.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions
+{
+ public static partial class MaybeExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static async Task> SelectMany(
+ this Task> maybeTask,
+ Func> selector,
+ Func project)
+ {
+ Maybe maybe = await maybeTask.DefaultAwait();
+ return maybe.SelectMany(selector, project);
+ }
+ }
+}
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Right.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Right.cs
new file mode 100644
index 00000000..fc6413e3
--- /dev/null
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Right.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions
+{
+ public static partial class MaybeExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static Task> SelectMany(
+ this Maybe maybe,
+ Func>> selector,
+ Func project)
+ {
+ return maybe
+ .Bind(selector)
+ .Map(x => project(maybe.Value, x));
+ }
+ }
+}
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.cs
new file mode 100644
index 00000000..cf2413f8
--- /dev/null
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions
+{
+ public static partial class MaybeExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static Task> SelectMany(
+ this Maybe maybe,
+ Func>> selector)
+ {
+ return maybe.Bind(selector);
+ }
+
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static Task> SelectMany(
+ this Task> maybeTask,
+ Func>> selector)
+ {
+ return maybeTask.Bind(selector);
+ }
+
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static async Task> SelectMany(
+ this Task> maybeTask,
+ Func>> selector,
+ Func project)
+ {
+ var maybe = await maybeTask.DefaultAwait();
+ return await maybe.SelectMany(selector, project).DefaultAwait();
+ }
+ }
+}
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Left.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Left.cs
new file mode 100644
index 00000000..bce4a8de
--- /dev/null
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Left.cs
@@ -0,0 +1,22 @@
+#if NET5_0_OR_GREATER
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions.ValueTasks
+{
+ public static partial class MaybeExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static async ValueTask> SelectMany(
+ this ValueTask> maybeTask,
+ Func> selector,
+ Func project)
+ {
+ Maybe maybe = await maybeTask;
+ return maybe.SelectMany(selector, project);
+ }
+ }
+}
+#endif
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Right.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Right.cs
new file mode 100644
index 00000000..77d52704
--- /dev/null
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Right.cs
@@ -0,0 +1,23 @@
+#if NET5_0_OR_GREATER
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions.ValueTasks
+{
+ public static partial class MaybeExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static ValueTask> SelectMany(
+ this Maybe maybe,
+ Func>> selector,
+ Func project)
+ {
+ return maybe
+ .Bind(selector)
+ .Map(x => project(maybe.Value, x));
+ }
+ }
+}
+#endif
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.cs
new file mode 100644
index 00000000..34cf3802
--- /dev/null
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.cs
@@ -0,0 +1,42 @@
+#if NET5_0_OR_GREATER
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions.ValueTasks
+{
+ public static partial class MaybeExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static ValueTask> SelectMany(
+ this Maybe maybe,
+ Func>> selector)
+ {
+ return maybe.Bind(selector);
+ }
+
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static ValueTask> SelectMany(
+ this ValueTask> maybeTask,
+ Func>> selector)
+ {
+ return maybeTask.Bind(selector);
+ }
+
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
+ public static async ValueTask> SelectMany(
+ this ValueTask> maybeTask,
+ Func>> selector,
+ Func project)
+ {
+ var maybe = await maybeTask;
+ return await maybe.SelectMany(selector, project);
+ }
+ }
+}
+#endif
diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.cs
index e914e8dd..18dff8a0 100644
--- a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.cs
+++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.cs
@@ -4,11 +4,17 @@ namespace CSharpFunctionalExtensions
{
public static partial class MaybeExtensions
{
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
public static Maybe SelectMany(in this Maybe maybe, Func> selector)
{
return maybe.Bind(selector);
}
+ ///
+ /// This method should be used in linq queries. We recommend using Bind method.
+ ///
public static Maybe SelectMany(in this Maybe maybe,
Func> selector,
Func project)
@@ -18,4 +24,4 @@ public static Maybe SelectMany(in this Maybe maybe,
Maybe.None);
}
}
-}
\ No newline at end of file
+}
diff --git a/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.Task.cs b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.Task.cs
new file mode 100644
index 00000000..697ebd0c
--- /dev/null
+++ b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.Task.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions
+{
+ public static partial class ResultExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Map method.
+ ///
+ public static Task> Select(this Task> result, Func selector)
+ {
+ return result.Map(selector);
+ }
+
+ ///
+ /// This method should be used in linq queries. We recommend using Map method.
+ ///
+ public static Task> Select(this Task> result, Func selector)
+ {
+ return result.Map(selector);
+ }
+ }
+}
diff --git a/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.ValueTask.cs b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.ValueTask.cs
new file mode 100644
index 00000000..af78bae4
--- /dev/null
+++ b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.ValueTask.cs
@@ -0,0 +1,26 @@
+#if NET5_0_OR_GREATER
+using System;
+using System.Threading.Tasks;
+
+namespace CSharpFunctionalExtensions.ValueTasks
+{
+ public static partial class ResultExtensions
+ {
+ ///
+ /// This method should be used in linq queries. We recommend using Map method.
+ ///
+ public static ValueTask> Select(in this ValueTask> result, Func selector)
+ {
+ return result.Map(selector);
+ }
+
+ ///
+ /// This method should be used in linq queries. We recommend using Map method.
+ ///
+ public static ValueTask> Select(in this ValueTask> result, Func selector)
+ {
+ return result.Map(selector);
+ }
+ }
+}
+#endif
diff --git a/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.cs b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.cs
index a5b98853..90a8e3bb 100644
--- a/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.cs
+++ b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.cs
@@ -11,5 +11,13 @@ public static Result Select(in this Result result, Func select
{
return result.Map(selector);
}
+
+ ///
+ /// This method should be used in linq queries. We recommend using Map method.
+ ///
+ public static Result Select(in this Result result, Func selector)
+ {
+ return result.Map(selector);
+ }
}
}
diff --git a/README.md b/README.md
index 254dfd02..fd675e2f 100644
--- a/README.md
+++ b/README.md
@@ -583,6 +583,75 @@ Console.WriteLine(appleInventory.MapError(ErrorEnhancer).ToString()); // "Succes
Console.WriteLine(bananaInventory.MapError(ErrorEnhancer).ToString()); // "Failed operation: Could not find any bananas"
```
+### Linq query syntax
+
+Lots of functional languages provide syntax sugars to make (Monadic) compositions of Maybe/Result types
+more readable, examples include:
+
+- Haskell's do-notation:
+ ```haskell
+ foo = do
+ x <- Just 1
+ y <- Just 2
+ return x + y
+ ```
+- Scala's for-comprehension:
+ ```scala
+ def foo = for {
+ x <- Some(1)
+ y <- Some(2)
+ } yield x + y
+ ```
+- F# computation expressions (`option` isn't in the stdlib, but it's trivial to define ourselves):
+ ```fsharp
+ let foo = option {
+ let! x = Some 1
+ let! y = Some 2
+ return x + y
+ }
+ ```
+
+C# wasn't designed as a functional language from the beginning, but surprisingly we can do the same using
+the Linq query syntax.
+
+Let's say the following `Create` factory methods do some validation and return `Result`.
+
+With method chaining:
+```csharp
+var customer = CustomerName
+ .Create("jsmith")
+ .Bind(name =>
+ Email.Create("jsmith@example.com").Map(email => new Customer(name, email))
+ );
+```
+
+Rewrite using the Linq query syntax:
+```csharp
+var customer =
+ from name in CustomerName.Create("jsmith")
+ from email in Email.Create("jsmith@example.com")
+ select new Customer(name, email);
+```
+
+They are technically the same, but the latter is more readable.
+
+And this also works with `async` methods:
+
+```csharp
+var billing = await (
+ from customer in _customerRepository.GetByIdAsync(id) // Task>
+ from billingInfo in _paymentGateway.ChargeCustomerAsync(customer, amount) // Task>
+ select billingInfo // Result
+);
+```
+
+Which is equivalent to:
+```csharp
+var billing = await _customerRepository
+ .GetByIdAsync(id)
+ .Bind(customer => _paymentGateway.ChargeCustomerAsync(customer, amount));
+```
+
## Testing
### [CSharpFunctionalExtensions.FluentAssertions](https://github.com/NitroDevs/CSharpFunctionalExtensions.FluentAssertions)