Skip to content

Commit 55267bb

Browse files
authored
Update Parsley.md
1 parent be60844 commit 55267bb

1 file changed

Lines changed: 136 additions & 169 deletions

File tree

Parsley.md

Lines changed: 136 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -6,215 +6,182 @@ NuGet\Install-Package Parsley.Net
66
```
77

88
## Implementation: Using Parsley.Net
9-
### Step 1. Create Aggregated Contract
10-
`Aggregated Contract` is the resultant response from all the aggregated apis. To create aggregated contract derive the class from `IContract` interface.
9+
### Step 1. Initialise and use Parser class.
10+
`Parser` is and implementation of `IParser` interface that provides methods for
11+
- parsing content of a file by specifying the file path
12+
- parsing an array of delimiter separated strings
1113

12-
Example.
14+
Please see below.
1315
```
14-
public class Customer : IContract
15-
{
16-
public int Id { get; set; }
17-
public string Code { get; set; }
18-
public string Name { get; set; }
19-
public Contacts Communication { get; set; }
20-
public Order[] Orders { get; set; }
21-
}
16+
public interface IParser
17+
{
18+
public T[] Parse<T>(string filepath) where T : IFileLine, new();
19+
public T[] Parse<T>(string[] lines) where T : IFileLine, new();
20+
}
2221
```
23-
### Step 2. Create Api Aggregated
24-
`Api Aggregate` is the composition of apis configured to obtain an aggregated data populated contract. To create an Api Aggregate derive from `ApiAggregate<TContract>` class where `TContract` is an implementation of IContract (ie. Agggregated Contract).
22+
To initialise `Parser` class you could do it manually or use dependency injection as shown below. The parser class has parameterised constructor that takes the delimiter character to initialise the instance. Default character is ',' (comma) to initialise the parser for a CSV file parsing.
2523

26-
Example.
24+
Example of Manual Instantiation is
2725
```
28-
internal class CustomerAggregate : ApiAggregate<Customer>
29-
{
30-
/// <summary>
31-
/// Constructs the api aggregate with web apis and result transformers to map data to aggregated contract.
32-
/// </summary>
33-
/// <returns>Mappings</returns>
34-
public override IEnumerable<Mapping<Customer, IApiResult>> Construct()
35-
{
36-
return CreateAggregate.For<Customer>()
37-
.Map<CustomerApi, CustomerTransform>(With.Name("customer"),
38-
customer => customer.Dependents
39-
.Map<CommunicationApi, CommunicationTransform>(With.Name("customer.communication"))
40-
.Map<OrdersApi, OrdersTransform>(With.Name("customer.orders"),
41-
customerOrders => customerOrders.Dependents
42-
.Map<OrderItemsApi, OrderItemsTransform>(With.Name("customer.orders.items")))
43-
).Create();
44-
}
45-
}
26+
var parser = new Parser('|');
4627
```
47-
`Api Aggregate` comprises of apis configured in hierarchical nested graph with each api having an associated result transformers. The result from the api is fed to the associated result transformer to map data to the aggregated contract.
48-
49-
### 2.1 Api & Transformer Pair
50-
Every `Api` type in the `ApiAggregate` definition should have a complementing `Transformer` type.
51-
- You need to assign a `name` to the `api/transformer` pair. See below rules for api naming convention.
28+
Example of IoC is
29+
```
30+
var services = new ServiceCollection();
5231
53-
Rules:
54-
* You could nest api/transformer pairs in a `parent/child` hierarchy. In a given parent/child hierarchy, the output of the parent api will serve as the input to the nested api to resolve its api endpoint.
55-
* The api/transformer mappings can be `nested` to `5` levels of dependency.
56-
* By convention, The api name should be `dot` separated string with a dot for every nested level. The name should includes all parent names separated by a dot for a given hierarchy.
57-
ie. For a 3 level dependency api mapping the name should be like `customer.orders.items`
32+
services.AddTransient(typeof(IParser), c => new Parser(','));
33+
// or use extension method
34+
services.UseParsley('|');
5835
59-
Example.
60-
> Below is the snippet from `CustomerAggregate` definition for parent/child relationship between Customer & Communication apis. The api response from CustomerApi is the input to CommunicationApi for resolving its endpoint url.
36+
serviceProvider = services.BuildServiceProvider();
37+
var parser = serviceProvider.GetService<IParser>();
6138
```
62-
.Map<CustomerApi, CustomerTransform>(With.Name("customer"), -- Parent mapping with name
63-
customer => customer.Dependents
64-
.Map<CommunicationApi, CommunicationTransform>(With.Name("customer.communication")) -- nested mapping with dot separated name
39+
### Step 2. Define the `IFileLine` implementation to parse a file record into a strongly typed line class.
40+
Consider the file below. To parse a row into a C# class, you need to implement `IFileLine` interface. By doing this you create a strongly typed line representation for each row in the file.
6541
```
66-
#### i. Web Api Class
67-
The purpose of a api class is to execute the web api with api engine to fetch the response.
68-
69-
As mentioned previously, You can configure an api in `Parent` or `Child` (nested) mode in a hierarchical graph.
70-
71-
To create `Web Api` defined as `parent` or `nested` api, you need to implement from `WebApi<TResult>` class,
72-
where `TResult` is `IApiResult` interface (or `ApiResult` base class) implementation and is the result that will be returned from executing the api.
42+
|Mr|Jack Marias|Male|London|
43+
|Dr|Bony Stringer|Male|New Jersey|
44+
|Mrs|Mary Ward|Female||
45+
|Mr|Robert Webb|||
46+
```
47+
Let us create an employee class which will hold data for each row shown in the file above. The properties in the line class should match to the column index and data type of the fields of the row.
7348

74-
Upon creating the web api class, you need to provide `GetUrl()` method implementation to return `Uri` instance.
75-
* Implement the `GetUrl(IRequestContext context, IApiResult parentApiResult)` method to return the constructed endpoint using given parameters of the method.
76-
* For `Parent Api`, only `IRequestContext` context parameter is passed to GetUrl() method to resolve the Url endpoint.
77-
* For `Nested Api`, api result parameter (ie. `IApiResult` parentApiResult) from the parent api is additionally passed in to GetUrl() method along with IRequestContext context parameter.
78-
* Optionally, override `GetRequestHeaders()` method to provide a dictionary of `outgoing headers` for the api request.
79-
* Optionally, override `GetResponseHeaders()` method to provide any list of `incoming headers` from the api response.
80-
* `IApiResult` implementation exposes `Headers` property for subscribed `response headers` received as part of the api response.
49+
We use the column attribute to specify the column index and can optionally specify a default value for the associated column should it be be empty. As a rule of thumb, the number of properties with column attributes should match the number of columns in the row else the engine will throw an exception.
8150

82-
`Important:`
83-
- The api `endpoint` needs to be resolved before executing the api with `ApiEngine`.
84-
- `IApiResult` parentApiResult parameter is null for apis configured in parent mode.
51+
IFileLine interface provides
52+
- `Index` property that holds the index of the parsed line relative to the whole file,
53+
- `Errors` property which is an array representing any column parsing failures.
8554

86-
Examples.
55+
```
56+
public interface IFileLine
57+
{
58+
public int Index { get; set; }
59+
public IList<string> Errors { get; set; }
60+
}
61+
```
8762

88-
> See example `CustomerApi` implemented to be configured and run in parent mode.
89-
```
90-
public class CustomerApi : WebApi<CustomerResult>
63+
Example.
64+
```
65+
public class Employee : IFileLine
9166
{
92-
public CustomerApi() : base(Endpoints.BaseAddress)
93-
{
94-
}
67+
// Custom column properties
9568
96-
// Override to construct the api endpoint.
97-
protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
98-
{
99-
// Executes as root or level 1 api. parentApiResult should be null.
100-
var customerContext = (CustomerContext)context;
69+
[Column(0)]
70+
public string Title { get; set; }
71+
[Column(1)]
72+
public string Name { get; set; }
73+
[Column(2)]
74+
public EnumGender Sex { get; set; }
75+
[Column(3, "London")]
76+
public string Location { get; set; }
10177
102-
return new Uri(string.Format(Endpoints.Customer, customerContext.CustomerId));
103-
}
78+
// IFileLine Members
79+
public int Index { get; set; }
80+
public IList<string> Errors { get; set; }
81+
}
82+
```
83+
Once you have created the line class it is as simple as calling one of the parser.Parse() methods below
10484

105-
// Override to pass custom outgoing headers with the api request.
106-
protected override IDictionary<string, string> GetRequestHeaders()
107-
{
108-
return new Dictionary<string, string>
109-
{
110-
{ "x-meta-branch-code", "Geneva" }
111-
};
112-
}
85+
i. By providing the path of the file to parse method.
86+
```
87+
var records = new Parser('|').Parse<Employee>("c://employees.txt");
88+
```
89+
ii. By providing the list of delimiter separated string values to parse method.
90+
```
91+
var lines = new[]
92+
{
93+
"|Mr|Jack Marias|Male|London|",
94+
"|Dr|Bony Stringer|Male|New Jersey|",
95+
};
11396
114-
// Override to get custom incoming headers with the api response.
115-
protected override IEnumerable<string> GetResponseHeaders()
116-
{
117-
return new[] { "x-meta-branch-code" };
118-
}
97+
var records = new Parser('|').Parse<Employee>(lines);
98+
```
99+
### Step 3. Advanced Parsing of data using nested types in the FileLine class.
100+
You could implement advance parsing of data by implementing `TypeConverter` class. Suppose we have to change the Name string property in Employee class above to a `NameType` property shown below.
101+
```
102+
public class Employee : IFileLine
103+
{
104+
[Column(0)]
105+
public string Title { get; set; }
106+
[Column(1)]
107+
public NameType Name { get; set; }
108+
[Column(2)]
109+
public EnumGender Sex { get; set; }
110+
[Column(3, "London")]
111+
public string Location { get; set; }
112+
113+
// IFileLine Members
114+
public int Index { get; set; }
115+
public IList<string> Errors { get; set; }
116+
}
117+
118+
public class NameType
119+
{
120+
public string FirstName { get; set; }
121+
public string Surname { get; set; }
119122
}
120123
```
121124

122-
> See example `CommunicationApi` implemented to be configured and run as nested api to customer api below.
125+
In order to parse the string name value from delimiter separated record in the file correctly to NameType instance, you need to implement custom `TypeConverter` converter.
126+
127+
Example - `NameConverter`
123128
```
124-
internal class CommunicationApi : WebApi<CommunicationResult>
129+
public class NameConverter : TypeConverter
130+
{
131+
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
125132
{
126-
public CommunicationApi() : base(Endpoints.BaseAddress)
127-
{
128-
}
129-
130-
protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
131-
{
132-
var customer = (CustomerResult)parentApiResult;
133-
return new Uri(string.Format(Endpoints.Communication, customer.Id));
134-
}
133+
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
135134
}
136-
```
137-
##### ii. Result Transformer Class
138-
The purpose of the transformer class is to map the response fetched by the linked api to the aggregated contract.
139-
140-
To define a transformer class, you need to implement `ResultTransformer<TResult, TContract>` class.
141-
- where TContract is Aggregated Contract implementing `IContract`. eg. Customer.
142-
- where TResult is Api Result from associated Query. It is an implementation of `IApiResult` interface.
143135
144-
Example.
136+
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
137+
{
138+
string stringValue;
139+
object result;
145140
146-
> `CustomerTransformer` is implemented to map `CustomerResult` recevied from CustomerApi to `Customer` Aggregated Contract.
141+
result = null;
142+
stringValue = value as string;
147143
148-
```
149-
public class CustomerTransform : ResultTransformer<CustomerResult, Customer>
150-
{
151-
public override void Transform(CustomerResult apiResult, Customer contract)
144+
if (!string.IsNullOrEmpty(stringValue))
152145
{
153-
var customer = contract ?? new Customer();
154-
customer.Id = apiResult.Id;
155-
customer.Name = apiResult.Name;
156-
customer.Code = apiResult.Code;
146+
result = NameType.Parse(stringValue);
157147
}
148+
149+
return result ?? base.ConvertFrom(context, culture, value);
158150
}
151+
}
159152
```
153+
After implementing the custom TypeConverter, you need to decorate the NameType class with ` [TypeConverter(typeof(NameConverter))]` attribute.
160154

161-
### Step 3. Parsley.Net Setup
162-
`Parsley.Net` needs to setup with required dependencies.
163-
164-
#### i. IoC Registrations
165-
With ServiceCollection, you need to register the below dependencies.
166155
```
167-
// Register core services
168-
services.AddTransient(typeof(IApiBuilder<>), typeof(ApiBuilder<>));
169-
services.AddTransient(typeof(IContractBuilder<>), typeof(ContractBuilder<>));
170-
services.AddTransient(typeof(IParsley.Net<>), typeof(Parsley.Net<>));
171-
services.AddTransient<IApiExecutor, ApiExecutor>();
172-
services.AddTransient<IApiEnginne, ApiEngine>();
156+
[TypeConverter(typeof(NameConverter))]
157+
public class NameType
158+
{
159+
public string FirstName { get; set; }
160+
public string Surname { get; set; }
173161
174-
// Register instance of IApiNameMatcher.
175-
services.AddTransient(c => new StringColonSeparatedMatcher());
162+
public static NameType Parse(string input)
163+
{
164+
var values = input.Split(' ');
176165
177-
// Enable logging
178-
services.AddLogging();
166+
if (values.Length == 1)
167+
return new NameType { FirstName = values[0] };
179168
180-
// Enable HttpClient
181-
services.AddHttpClient();
169+
if (values.Length == 2)
170+
return new NameType { FirstName = values[0], Surname = values[1] };
182171
183-
// Register api aggregate definitions. eg CustomerAggregate
184-
services.AddTransient<IApiAggregate<Customer>, CustomerAggregate>();
185-
```
186-
#### ii. With Fluent Registration Api
187-
You could also acheieve the above registrations using fluent registration below.
188-
```
189-
// Enable logging
190-
services.AddLogging();
172+
if (values.Length > 2)
173+
{
174+
var forenames = string.Empty;
175+
for (var i = 0; i < values.Length - 1; i++)
176+
forenames += string.Concat(values[i]) + " ";
191177
192-
// Enable HttpClient
193-
services.AddHttpClient();
178+
return new NameType { FirstName = forenames.Trim(), Surname = values[values.Length - 1] };
179+
}
194180
195-
// Fluent registration.
196-
services.UseParsley.Net()
197-
.AddApiAggregate<Customer>(new CustomerAggregate());
181+
return new NameType { FirstName = input };
182+
}
183+
}
198184
```
199-
Note: Above examples to enable HttpClient is basic. However, you could additionally implement circuit breaking & retry policies.
200-
>Please see [`Circuit Breaker`](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-circuit-breaker-pattern) pattern for more details.
201-
202-
203-
### Step 4. Use IParsley.Net<`TContract`>
204-
205-
#### i. IApiAggrgator (DI)
206-
To use Api aggregator, Inject IApiAggrgator<TContract> where TContract is IContract, using constructor & property injection method or explicity Resolve using service provider
207-
208-
Example. `IServiceProvider.GetService(typeof(IApiAggrgator<Customer>))`
185+
Now parsing the file should hydrate data correctly to the FileLine class and its nested name type.
209186

210-
#### ii. Call Aggregator.GetData() method
211-
You need to call the `GetData()` method with an instance of parameter class derived from `IRequestContext` interface.
212-
- The IRequestContext provides a `Names` property which is a list of string to include all the api names to be included for the given request to fetch aggregated response.
213-
- When `no` names are passed in the paramter then `entire` aggregated response for all configured apis is returned.
214-
- When `subset` of apis are included using names then the returned aggregated response only includes `api responses` from included apis.
215-
- When nested api with `dot` separated api name (eg. `customer.orders.items`) is included then all parent apis also get included for the dependency.
216-
217-
##### Example - Control Flow
218-
> Example execution flow for a nested api included in the GetData() parameter.
219-
<img src="https://github.com/CodeShayk/Parsley.Net/blob/master/Images/Parsley.Net.ControlFlow.png" alt="control-flow"/>
220187

0 commit comments

Comments
 (0)