Skip to content

Commit 27f9b7d

Browse files
committed
improve linq query support
1 parent 15a6158 commit 27f9b7d

File tree

14 files changed

+521
-1
lines changed

14 files changed

+521
-1
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using System;
2+
using System.Text.RegularExpressions;
3+
using System.Threading.Tasks;
4+
5+
namespace CSharpFunctionalExtensions.Examples.LinqQuery
6+
{
7+
/// <summary>
8+
/// Demonstrates using LINQ query syntax (sugar) as an alternative to
9+
/// fluent method chaining to compose operations in a monadic style,
10+
/// analogous to F# computation expressions or Haskell's do-notation.
11+
/// </summary>
12+
public class ResultQueryExamples
13+
{
14+
public async Task Demo()
15+
{
16+
// Using the LINQ query syntax:
17+
var validCustomer =
18+
from email in Email.Create("[email protected]")
19+
from name in CustomerName.Create("jsmith")
20+
select new Customer(name, email);
21+
22+
// Equivalent to:
23+
24+
var validCustomer_ = Email
25+
.Create("[email protected]")
26+
.Bind(email =>
27+
CustomerName.Create("jsmith").Map(name => new Customer(name, email))
28+
);
29+
30+
// Success(Customer(Name: jsmith, Email: [email protected]))
31+
Console.WriteLine(validCustomer);
32+
33+
var invalidCustomer =
34+
from email in Email.Create("no email")
35+
from name in CustomerName.Create("jsmith")
36+
select new Customer(name, email);
37+
38+
// Failure(E-mail is invalid)
39+
Console.WriteLine(invalidCustomer);
40+
41+
//------------------------------------------------------------------------------
42+
// Also works with async methods.
43+
44+
var billing = await (
45+
from customer in CustomerRepository.GetByIdAsync(1) // Task<Result<Customer>>
46+
from billingInfo in PaymentGateway.ChargeCustomerAsync(customer, 1_000) // Task<Result<BillingInfo>>
47+
select billingInfo
48+
);
49+
50+
// Equivalent to:
51+
var billing_ = await CustomerRepository
52+
.GetByIdAsync(1)
53+
.Bind(customer => PaymentGateway.ChargeCustomerAsync(customer, 1_000));
54+
55+
// Success(BillingInfo(Customer: [email protected], ChargeAmount: 1000))
56+
Console.WriteLine(billing);
57+
58+
var failedBilling = await (
59+
from customer in CustomerRepository.GetByIdAsync(1)
60+
from billingInfo in PaymentGateway.ChargeCustomerAsync(customer, 5_000_000)
61+
select billingInfo
62+
);
63+
64+
// Failure(Insufficient balance.)
65+
Console.WriteLine(failedBilling);
66+
}
67+
}
68+
69+
public class Email
70+
{
71+
private readonly string _value;
72+
73+
private Email(string value)
74+
{
75+
_value = value;
76+
}
77+
78+
public override string ToString()
79+
{
80+
return _value;
81+
}
82+
83+
public static Result<Email> Create(string email)
84+
{
85+
if (string.IsNullOrWhiteSpace(email))
86+
return Result.Failure<Email>("E-mail can't be empty");
87+
88+
if (email.Length > 100)
89+
return Result.Failure<Email>("E-mail is too long");
90+
91+
if (!Regex.IsMatch(email, @"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$"))
92+
return Result.Failure<Email>("E-mail is invalid");
93+
94+
return Result.Success(new Email(email));
95+
}
96+
}
97+
98+
public class CustomerName
99+
{
100+
private readonly string _value;
101+
102+
private CustomerName(string value)
103+
{
104+
_value = value;
105+
}
106+
107+
public override string ToString()
108+
{
109+
return _value;
110+
}
111+
112+
public static Result<CustomerName> Create(string name)
113+
{
114+
if (string.IsNullOrWhiteSpace(name))
115+
return Result.Failure<CustomerName>("Name can't be empty");
116+
117+
if (name.Length > 50)
118+
return Result.Failure<CustomerName>("Name is too long");
119+
120+
return Result.Success(new CustomerName(name));
121+
}
122+
}
123+
124+
public class Customer
125+
{
126+
public CustomerName Name { get; private set; }
127+
public Email Email { get; private set; }
128+
129+
public Customer(CustomerName name, Email email)
130+
{
131+
if (name == null)
132+
throw new ArgumentNullException("name");
133+
if (email == null)
134+
throw new ArgumentNullException("email");
135+
136+
Name = name;
137+
Email = email;
138+
}
139+
140+
public override string ToString()
141+
{
142+
return $"{nameof(Customer)}({nameof(Name)}: {Name}, {nameof(Email)}: {Email})";
143+
}
144+
}
145+
146+
public class CustomerRepository
147+
{
148+
public static async Task<Result<Customer>> GetByIdAsync(int id)
149+
{
150+
var customer =
151+
from email in Email.Create("[email protected]")
152+
from name in CustomerName.Create("jsmith")
153+
select new Customer(name, email);
154+
155+
return customer;
156+
}
157+
}
158+
159+
public class BillingInfo
160+
{
161+
public Customer Customer { get; set; }
162+
public decimal ChargeAmount { get; set; }
163+
164+
public override string ToString()
165+
{
166+
return $"{nameof(BillingInfo)}({nameof(Customer)}: {Customer.Email}, {nameof(ChargeAmount)}: {ChargeAmount})";
167+
}
168+
}
169+
170+
public class PaymentGateway
171+
{
172+
public static async Task<Result<BillingInfo>> ChargeCustomerAsync(
173+
Customer customer,
174+
decimal chargeAmount
175+
)
176+
{
177+
if (chargeAmount > 1_000_000)
178+
return Result.Failure<BillingInfo>("Insufficient balance");
179+
180+
return Result.Success(
181+
new BillingInfo { Customer = customer, ChargeAmount = chargeAmount }
182+
);
183+
}
184+
}
185+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace CSharpFunctionalExtensions
5+
{
6+
public static partial class MaybeExtensions
7+
{
8+
/// <summary>
9+
/// This method should be used in linq queries. We recommend using Map method.
10+
/// </summary>
11+
public static Task<Maybe<K>> Select<T, K>(this Task<Maybe<T>> maybe, Func<T, K> selector)
12+
{
13+
return maybe.Map(selector);
14+
}
15+
}
16+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#if NET5_0_OR_GREATER
2+
using System;
3+
using System.Threading.Tasks;
4+
5+
namespace CSharpFunctionalExtensions.ValueTasks
6+
{
7+
public static partial class MaybeExtensions
8+
{
9+
/// <summary>
10+
/// This method should be used in linq queries. We recommend using Map method.
11+
/// </summary>
12+
public static ValueTask<Maybe<K>> Select<T, K>(in this ValueTask<Maybe<T>> maybe, Func<T, K> selector)
13+
{
14+
return maybe.Map(selector);
15+
}
16+
}
17+
}
18+
#endif
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace CSharpFunctionalExtensions
5+
{
6+
public static partial class MaybeExtensions
7+
{
8+
/// <summary>
9+
/// This method should be used in linq queries. We recommend using Bind method.
10+
/// </summary>
11+
public static async Task<Maybe<V>> SelectMany<T, U, V>(
12+
this Task<Maybe<T>> maybeTask,
13+
Func<T, Maybe<U>> selector,
14+
Func<T, U, V> project)
15+
{
16+
Maybe<T> maybe = await maybeTask.DefaultAwait();
17+
return maybe.SelectMany(selector, project);
18+
}
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace CSharpFunctionalExtensions
5+
{
6+
public static partial class MaybeExtensions
7+
{
8+
/// <summary>
9+
/// This method should be used in linq queries. We recommend using Bind method.
10+
/// </summary>
11+
public static Task<Maybe<V>> SelectMany<T, U, V>(
12+
this Maybe<T> maybe,
13+
Func<T, Task<Maybe<U>>> selector,
14+
Func<T, U, V> project)
15+
{
16+
return maybe
17+
.Bind(selector)
18+
.Map(x => project(maybe.Value, x));
19+
}
20+
}
21+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace CSharpFunctionalExtensions
5+
{
6+
public static partial class MaybeExtensions
7+
{
8+
/// <summary>
9+
/// This method should be used in linq queries. We recommend using Bind method.
10+
/// </summary>
11+
public static Task<Maybe<K>> SelectMany<T, K>(
12+
this Maybe<T> maybe,
13+
Func<T, Task<Maybe<K>>> selector)
14+
{
15+
return maybe.Bind(selector);
16+
}
17+
18+
/// <summary>
19+
/// This method should be used in linq queries. We recommend using Bind method.
20+
/// </summary>
21+
public static Task<Maybe<K>> SelectMany<T, K>(
22+
this Task<Maybe<T>> maybeTask,
23+
Func<T, Task<Maybe<K>>> selector)
24+
{
25+
return maybeTask.Bind(selector);
26+
}
27+
28+
/// <summary>
29+
/// This method should be used in linq queries. We recommend using Bind method.
30+
/// </summary>
31+
public static async Task<Maybe<V>> SelectMany<T, U, V>(
32+
this Task<Maybe<T>> maybeTask,
33+
Func<T, Task<Maybe<U>>> selector,
34+
Func<T, U, V> project)
35+
{
36+
var maybe = await maybeTask.DefaultAwait();
37+
return await maybe.SelectMany(selector, project).DefaultAwait();
38+
}
39+
}
40+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#if NET5_0_OR_GREATER
2+
using System;
3+
using System.Threading.Tasks;
4+
5+
namespace CSharpFunctionalExtensions.ValueTasks
6+
{
7+
public static partial class MaybeExtensions
8+
{
9+
/// <summary>
10+
/// This method should be used in linq queries. We recommend using Bind method.
11+
/// </summary>
12+
public static async ValueTask<Maybe<V>> SelectMany<T, U, V>(
13+
this ValueTask<Maybe<T>> maybeTask,
14+
Func<T, Maybe<U>> selector,
15+
Func<T, U, V> project)
16+
{
17+
Maybe<T> maybe = await maybeTask;
18+
return maybe.SelectMany(selector, project);
19+
}
20+
}
21+
}
22+
#endif
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#if NET5_0_OR_GREATER
2+
using System;
3+
using System.Threading.Tasks;
4+
5+
namespace CSharpFunctionalExtensions.ValueTasks
6+
{
7+
public static partial class MaybeExtensions
8+
{
9+
/// <summary>
10+
/// This method should be used in linq queries. We recommend using Bind method.
11+
/// </summary>
12+
public static ValueTask<Maybe<V>> SelectMany<T, U, V>(
13+
this Maybe<T> maybe,
14+
Func<T, ValueTask<Maybe<U>>> selector,
15+
Func<T, U, V> project)
16+
{
17+
return maybe
18+
.Bind(selector)
19+
.Map(x => project(maybe.Value, x));
20+
}
21+
}
22+
}
23+
#endif
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#if NET5_0_OR_GREATER
2+
using System;
3+
using System.Threading.Tasks;
4+
5+
namespace CSharpFunctionalExtensions.ValueTasks
6+
{
7+
public static partial class MaybeExtensions
8+
{
9+
/// <summary>
10+
/// This method should be used in linq queries. We recommend using Bind method.
11+
/// </summary>
12+
public static ValueTask<Maybe<K>> SelectMany<T, K>(
13+
this Maybe<T> maybe,
14+
Func<T, ValueTask<Maybe<K>>> selector)
15+
{
16+
return maybe.Bind(selector);
17+
}
18+
19+
/// <summary>
20+
/// This method should be used in linq queries. We recommend using Bind method.
21+
/// </summary>
22+
public static ValueTask<Maybe<K>> SelectMany<T, K>(
23+
this ValueTask<Maybe<T>> maybeTask,
24+
Func<T, ValueTask<Maybe<K>>> selector)
25+
{
26+
return maybeTask.Bind(selector);
27+
}
28+
29+
/// <summary>
30+
/// This method should be used in linq queries. We recommend using Bind method.
31+
/// </summary>
32+
public static async ValueTask<Maybe<V>> SelectMany<T, U, V>(
33+
this ValueTask<Maybe<T>> maybeTask,
34+
Func<T, ValueTask<Maybe<U>>> selector,
35+
Func<T, U, V> project)
36+
{
37+
var maybe = await maybeTask;
38+
return await maybe.SelectMany(selector, project);
39+
}
40+
}
41+
}
42+
#endif

0 commit comments

Comments
 (0)