├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── ROP.sln ├── bump-config.json ├── src ├── ROP.ApiExtensions.Translations │ ├── Language │ │ ├── CultureScope.cs │ │ ├── Extensions │ │ │ └── AcceptedLanguageExtension.cs │ │ └── LocalizationUtils.cs │ ├── ROP.ApiExtensions.Translations.csproj │ ├── RopTranslationsExtensions.cs │ └── Serializers │ │ └── ErrorDtoSerializer.cs ├── ROP.ApiExtensions │ ├── ActionResultExtensions.cs │ ├── ErrorDto.cs │ ├── ErrorDtoExtensions.cs │ ├── ROP.ApiExtensions.csproj │ └── ResultDto.cs └── ROP │ ├── Error.cs │ ├── ErrorResultException.cs │ ├── IEnumerableUtils.cs │ ├── ROP.csproj │ ├── Result.cs │ ├── ResultFailure │ ├── Result_BadRequest.cs │ ├── Result_Conflict.cs │ ├── Result_Failure.cs │ └── Result_NotFound.cs │ ├── Result_Bind.cs │ ├── Result_Combine.cs │ ├── Result_Fallback.cs │ ├── Result_Ignore.cs │ ├── Result_Map.cs │ ├── Result_SuccessHTTPStatusCode.cs │ ├── Result_T.cs │ ├── Result_Then.cs │ ├── Result_Throw.cs │ ├── Result_Traverse.cs │ └── Unit.cs └── test ├── ROP.Ejemplo.CasoDeUso ├── AddUser │ ├── AddUserPOOService.cs │ └── AdduserROPService.cs ├── DTO │ └── UserAccount.cs └── ROP.Ejemplo.CasoDeUso.csproj └── ROP.UnitTest ├── ApiExtensions └── Translations │ ├── ErrorTranslations.cs │ ├── ErrorTranslations.en.resx │ └── TestErrorDtoSerializerSerialization.cs ├── BaseResultTest.cs ├── ROP.UnitTest.csproj ├── ResultFailure ├── TestBaseResultFailure.cs ├── TestResultBadRequest.cs ├── TestResultConflict.cs ├── TestResultFailure.cs └── TestResultNotFound.cs ├── Serializer └── TestResultDtoOnSerialization.cs ├── TestActionResultExtensions.cs ├── TestAsync.cs ├── TestBind.cs ├── TestCombine.cs ├── TestFallback.cs ├── TestIgnore.cs ├── TestMap.cs ├── TestSuccessHTTPStatusCode.cs ├── TestThen.cs └── TestTraverse.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | Pipeline: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: 8.0.x 19 | 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | 23 | - name: Build 24 | run: dotnet build --no-restore 25 | 26 | - name: Test 27 | run: dotnet test --no-build --verbosity normal 28 | 29 | - name: Install necessary tools 30 | run: sudo apt-get update && sudo apt-get install -y xmlstarlet 31 | 32 | - name: Read current version from .csproj 33 | run: | 34 | VERSION=$(xmlstarlet sel -t -v "//Project/PropertyGroup/Version" src/ROP/ROP.csproj) 35 | echo "VERSION=$VERSION" >> $GITHUB_ENV 36 | echo "Current version: $VERSION" 37 | 38 | - name: Determine bump type 39 | run: | 40 | IS_MAJOR=$(jq -r '.isMajor // false' bump-config.json) 41 | IS_MINOR=$(jq -r '.isMinor // false' bump-config.json) 42 | IS_PATCH=$(jq -r '.isPatch // false' bump-config.json) 43 | 44 | if [ "$IS_MAJOR" = "true" ]; then 45 | BUMP_TYPE="major" 46 | elif [ "$IS_MINOR" = "true" ]; then 47 | BUMP_TYPE="minor" 48 | else 49 | BUMP_TYPE="patch" 50 | fi 51 | echo "Bump type: $BUMP_TYPE" 52 | echo "BUMP_TYPE=$BUMP_TYPE" >> $GITHUB_ENV 53 | 54 | - name: Calculate new version 55 | run: | 56 | IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" 57 | case "$BUMP_TYPE" in 58 | major) 59 | NEW_VERSION="$((MAJOR + 1)).0.0" 60 | ;; 61 | minor) 62 | NEW_VERSION="$MAJOR.$((MINOR + 1)).0" 63 | ;; 64 | patch) 65 | NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" 66 | ;; 67 | esac 68 | echo "New version: $NEW_VERSION" 69 | echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV 70 | 71 | - name: Set project paths 72 | run: echo "PROJECT_PATHS=src/ROP/ROP.csproj src/ROP.ApiExtensions/ROP.ApiExtensions.csproj src/ROP.ApiExtensions.Translations/ROP.ApiExtensions.Translations.csproj" >> $GITHUB_ENV 73 | 74 | - name: Update all .csproj files with new version 75 | run: | 76 | for proj in $PROJECT_PATHS 77 | do 78 | xmlstarlet ed -L -u "//Version" -v "$NEW_VERSION" "$proj" 79 | sed -i '1{/^` structure. 19 | - `Netmentor.ROP.ApiExtensions`: Provides `ToActionResult` that converts `Result` into `IActionResult`. 20 | - `Netmentor.ROP.ApiExtensions.Translations`: Provides the ability to use your `.resx` files for your error messages for multiple languages. 21 | 22 | 23 | 24 | ## Implement Railway Oriented programming into your application 25 | You can find the `Result` structure in Nexus, including the package `Netmentor.Rop"`. 26 | 27 | The `Result` type is an immutable type that allows you to store success or errors, not both at the same time. It makes no sense to have errors where the result was a success or vice versa. 28 | 29 | And `Result` is immutable because to mutate it, you have to use the framework built around it. For this reason, you have to use the extension methods available. If you feel like you need something else, please create a PR for it. 30 | - Note 1: Remember that `void` is not a Type in C#, and we don't like non-typed stuff. For that reason, if in any method you feel that you don't want to return a type, you have to use the type called `Unit`, which is standard in the functional world. 31 | - Note 2: We designed the framework to support both async and sync workflows. 32 | 33 | The `Result` structure contains three properties 34 | 1. `T` The value, populated if there are no errors. 35 | 2. `ImmutableArray` Array of errors, empty if none. 36 | 3. `Success` Boolean indicates if the result is in a success status (no errors). 37 | 38 | 39 | ### Return a `Result` 40 | 41 | To return a `Result`, you only need to have your method return a `Result` and a custom [implicit operator](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators) will do the rest. 42 | 43 | So, for example, if you want to return an int, you will normally do something like: 44 | ````csharp 45 | public int GetMyInt(){ 46 | return 1; 47 | } 48 | ```` 49 | 50 | Thanks to the implicit operator, you can do pretty much the same, change the return type 51 | ```csharp 52 | public Result GetMyInt() 53 | { 54 | return 1; 55 | } 56 | ``` 57 | Unfortunately, it is slightly different when you are returning `Task>`; If you have any `await` in the code, it is fine. but if you don't, you have to specify `Success().Async()` when returning the value: 58 | ```csharp 59 | public Task> GetMyInt() 60 | { 61 | return 1.Success().Async(); 62 | } 63 | ``` 64 | This is because we cannot have implicit task operators. 65 | 66 | #### Use case 67 | If we apply this logic into our use case, we can have the validation method looking like this: 68 | ````csharp 69 | private Result ValidateNewAccount(Account account) 70 | { 71 | return true; 72 | } 73 | ```` 74 | 75 | ### Return an error 76 | We just saw how to return something that works, but what if something fails? We have to return an error. 77 | For this, we will use `Result.Failure("error")`. 78 | Modify the method to see the example: 79 | ````csharp 80 | private Result ValidateNewAccount(Account account) 81 | { 82 | if(string.IsNullOrWhitespace(account.FirstName) 83 | return Result.Failure("first name must be populated") 84 | 85 | if(string.IsNullOrWhitespace(account.LastName) 86 | return Result.Failure("Last name must be populated") 87 | 88 | return account; 89 | } 90 | ```` 91 | As you can see, you check every one of them and, if any fails, returns a "failure", which, if you remember our previous images, will move the flow to the "non-happy" path. 92 | - Note: if you need to return all the errors, you can make a `List` and a return like the next: `return Result.Failure(listOferrors);` 93 | 94 | ### Calling the second method 95 | 96 | We have just seen what our method look like, but how do we call and link the second one? 97 | 98 | We have to understand the `Result` as a chain, which means that one response is linked to the following input. 99 | 100 | To accomplish the "call" to the next method we use the extension method called `.Bind`: 101 | ``` csharp 102 | public Result BasicAccountCreation(Account account) 103 | { 104 | return ValidateNewAccount(account) 105 | .Bind(SaveUser) 106 | .Bind(SendCode); 107 | } 108 | ``` 109 | Bind is using a [delegate](https://www.netmentor.es/Entrada/delegados-csharp) `Func>` to receive the method as a parameter. 110 | 111 | This also means that you can modify the input. Just imagine that `ValidateNewAccount` returns a bool, but you don't require the bool because you need the user to save the user. in this case, you can use a lambda expression to discard the result and pass the account: 112 | ````csharp 113 | return ValidateNewAccount(account) 114 | .Bind(_ => SaveUser(account)) 115 | ```` 116 | 117 | Also, in our case, `SaveUser` will be `Async` as it is storing information in the database. For that reason, we must convert our chain to `Async`: 118 | ``` csharp 119 | public async Task> BasicAccountCreation(Account account) 120 | { 121 | return await ValidateNewAccount(account) 122 | .Async() //<- this line here 123 | .Bind(SaveUser) 124 | .Bind(SendCode); 125 | } 126 | ``` 127 | 128 | #### Implementing the second method 129 | I mentioned before that the output of the first method is the input for the second. This is true, but not entirely, as our method returns `Result` in our input. We don't want to use `Result`. We only need `T`. 130 | 131 | And that is how it works. Our `SaveUser` method will be something like the following. 132 | ````csharp 133 | private async Task> SaveUser(Account account) 134 | { 135 | return await _dependency.SaveUser(account) 136 | .Map(_=>account.email); 137 | } 138 | //This will be the implementation of the third method. 139 | private async Task> SendCode(string email) 140 | { 141 | return await _dependencies.SendEmail(email); 142 | } 143 | ```` 144 | Everything related to `Result` is managed by the library automatically. 145 | 146 | #### How Bind works internally 147 | The following example is only for Bind, but all the extension methods have the same pattern. 148 | ````csharp 149 | public static Result Bind(this Result r, Func> method) 150 | { 151 | try 152 | { 153 | return r.Success 154 | ? method(r.Value) 155 | : Result.Failure(r.Errors, r.HttpStatusCode); 156 | } 157 | catch (Exception e) 158 | { 159 | ExceptionDispatchInfo.Capture(e).Throw(); 160 | throw; 161 | } 162 | } 163 | ```` 164 | As you can see is an extension method of `Result` which checks if it is a success; 165 | - If it is a success, execute the method 166 | - If it is not a success, it returns the failure errors. (sidenote: we will explain the "StatusCode" later). 167 | 168 | Also, don't forget that you can have chains inside chains. 169 | 170 | ### Usage of the Library 171 | 172 | As mention, I designed the library to cover all the use cases I use it for, but it could be more, do not heasitate and create a PR! 173 | 174 | These are the extension methods you can use in your chain. 175 | 176 | #### Bind 177 | Allows you to link two methods. Example: 178 | ````csharp 179 | return ValidateNewAccount(account) 180 | .Bind(SaveUser) 181 | .Bind(SendCode); 182 | ```` 183 | #### Fallback 184 | The `Fallback` method is executed when the previous method has a failure response. 185 | 186 | Example: 187 | ````csharp 188 | return ValidateNewAccount(account) 189 | .Bind(SaveUser) //<- Assuming this fails 190 | .Fallback(SaveInAnotherDatabase) //<- then this is executed if it didnt failed, it does not get executed. 191 | .Bind(SendCode); 192 | ```` 193 | 194 | #### Combine 195 | 196 | Allows you to combine two different responses 197 | ````csharp 198 | return ValidateNewAccount(account) 199 | .Bind(SaveUser) 200 | .Combine(SaveInAnotherDatabase) 201 | .Bind(AnotherMethod); 202 | ```` 203 | In this case, `AnotherMethod` will receive the response of both previous methods in a tuple. 204 | 205 | #### Ignore 206 | Sometimes, the use case will contain certain logic, this logic may not affect the rest of the use case. If that is the case, you can use `.Ignore()` and it will convert your `Result` into `Result`. 207 | ````csharp 208 | Result result = ValidateNewAccount(account) 209 | .Bind(SaveUser) 210 | .Ignore(); 211 | ```` 212 | 213 | #### Map 214 | Allow us to specify a method which is not returning a `Result` into the chain. A mapper will be the most common usecase. 215 | ````csharp 216 | Result result = ValidateNewAccount(account) 217 | .Bind(SaveUser) 218 | .Map(_dependency.MapToNetmentor); //<- being this something like IMapper 219 | ```` 220 | 221 | #### Then 222 | Instead of using a delegate `Func>` in this case uses an `Action` which means that the result of this then will be ignored. 223 | ````csharp 224 | Result result = ValidateNewAccount(account) 225 | .Bind(SaveUser) 226 | .Then(TriggerExternalService) 227 | .Bind(SendCode); 228 | ```` 229 | The use case will be similar to a fire&Forget. 230 | 231 | #### Throw 232 | It will return to you the actual value of `T` if it is in a success status, but if is not success, it will throw a `ErrorResultException` with the errors as message on the exception. 233 | ````csharp 234 | NetMentorAccount result = ValidateNewAccount(account) 235 | .Bind(SaveUser) 236 | .Map(_dependency.MapToNetmentor); 237 | .Throw() 238 | ```` 239 | You will not need to call this in 99.9% of the use case. 240 | 241 | #### Traverse 242 | Converts a list of `IEnumerable>` into `Result>` 243 | `````csharp 244 | Result> result = GetUsersByIds(arrayIds) //<- assuing GetUsersByIds returns List> 245 | .Traverse(); 246 | ````` 247 | 248 | #### UseSuccessHttpStatusCode 249 | In most of our use cases, we return the information using an API, and it is not always the same status code, so for that reason, we allow to change this status code when the chain is a success. 250 | ````csharp 251 | Result result = ValidateNewAccount(account) 252 | .UseSuccessHttpStatusCode(HttpStatusCode.Accepted); 253 | ```` 254 | If you don't specify any status code, it will be `HttpStatusCode.OK` (200). 255 | 256 | But as mentioned, this only matters if you are using API. 257 | 258 | ### The Error type 259 | 260 | For the errors, we also created a type. This type contains three properties. 261 | - `ErorMessage` allows you to introduce a message for the errors. 262 | - `ErrorCode` Code set by you in your application which references the error. 263 | 264 | To create the errors you have to use the factory method: `Error Create(string message, Guid? errorCode = null)`. 265 | 266 | But when you do `Result.Failure("errorMessage")` it creates the error automatically. 267 | 268 | #### Error status codes 269 | As there is a possibility with `UseSuccessHttpStatusCode` of setting up a success status code, there is also a way to set up a failure status code. 270 | 271 | If you remember, when we create an error, we do it with `Result.Failure(Error)` this generates a status code of `HttpStatusCode.UnprocessableEntity` (422) 272 | 273 | But what if you want another error code? 274 | 275 | At the moment we support 3 additional Error codes 276 | - `HttpStatusCode.BadRequest` (400) 277 | `````csharp 278 | Result.BadRequest(Error) 279 | ````` 280 | - `HttpStatusCode.NotFound` (404) 281 | `````csharp 282 | Result.NotFound(Error) 283 | ````` 284 | - `HttpStatusCode.Conflict` (409) 285 | `````csharp 286 | Result.Conflict(Error) 287 | ````` 288 | 289 | ### Response from an API 290 | As we mentioned, most of our use cases are API calls, which means we have to return this information from an API. 291 | 292 | To achieve this goal first of all you have to install the package `Common.API` from nexus. 293 | 294 | You have to call the extension method `ToActionResult`, which will automatically translate the struct into the `IActionResult`. 295 | ````chsarp 296 | public async Task CreateNewAccount(Account account) 297 | { 298 | return await _useCase.Execute(account) 299 | .ToActionResult(); 300 | } 301 | ```` 302 | If `Result` is a success, it will return the status code set with `UseSuccessHttpStatusCode` (or the default 422 if not set), and if the `Result` is not successful, it will return the one set when you specified the error. 303 | 304 | #### ResultDto 305 | During the execution of `.ToActionResult()` result gets converted into a `Data Transfer Object` (Dto) for that reason it returns a `ResultDto`. 306 | - Note: We mainly did it due to serialisation issues with immutable classes in C#. 307 | 308 | 309 | ### ProblemDetails 310 | if what you are looking for is convert any non success Status Code into a [ProblemDetails](https://datatracker.ietf.org/doc/html/rfc9457) object you can do it with `.ToResultOrProblemDetails`: 311 | 312 | ````chsarp 313 | public async Task CreateNewAccount(Account account) 314 | { 315 | return await _useCase.Execute(account) 316 | .ToResultOrProblemDetails(); 317 | } 318 | ```` 319 | 320 | This will return either `T` for any successful response or ProblemDetails with the `Status` property as the `Statuscode` 321 | and a property called `Errors` that contains a list of errors: 322 | 323 | ```json 324 | { 325 | "value": { 326 | "title": "Error(s) found", 327 | "status": 400, 328 | "detail": "One or more errors occurred", 329 | "validationErrors": [ 330 | { 331 | "message": "example message", 332 | "errorCode": "0b002212-4e6b-4561-96f7-8a06dbc65ac3", 333 | "translationVariables": null 334 | } 335 | ] 336 | }, 337 | "formatters": [], 338 | "contentTypes": [], 339 | "declaredType": null, 340 | "statusCode": 400 341 | } 342 | ``` 343 | 344 | 345 | 346 | ## Error Translations 347 | The library can be extended with `ROP.ApiExtensions.Translations` to provide an out of the box functionality to translate errors at serialization time. 348 | 349 | You need to create a resource per language you want to support and an empty class with the same name. 350 | Then If there is an error to be returned in your API it will translate it automatically **IF** the message was left blank. 351 | 352 | For example If you Create an error like the following: 353 | ````csharp 354 | return Result.Failure(Guid.Parse("ce6887fb-f8fa-49b7-bcb4-d8538b6c9932")).ToActionResult() 355 | ```` 356 | It will look for that guid in the translation file and show the message field as this exampole: 357 | 358 | ````json 359 | { 360 | "value": null, 361 | "errors": [ 362 | { 363 | "ErrorCode": "ce6887fb-f8fa-49b7-bcb4-d8538b6c9932", 364 | "Message": "Example message" 365 | } 366 | ], 367 | "success": false 368 | } 369 | ```` 370 | 371 | ### Error Translations with variables 372 | The messages in the translations support variables on the messages using standard C# string format. 373 | 374 | For example a message like `example variable1: {0}, variable2: {2}` will replace `{x}` for the variables you assign on the error creation. 375 | 376 | To accomplish the expected result, all the failure creation messages that support the `ErrorCode` have an optional parameter that accpets an array of string (`string[]`) 377 | which should match the number of variables in the string message, example: 378 | 379 | ````csharp 380 | Result.NotFound("Guid-NOT-found", new []{"identifier1"}); // "The id {0} cannot be found" 381 | -> "The id identifier1 cannot be found" 382 | ```` 383 | 384 | 385 | Note: If the `TranslationVariables` is empty the library will not try to format the string message. 386 | 387 | 388 | 389 | ### Add the custom serializer. 390 | 391 | You can add it manually with the next command: 392 | ````csharp 393 | services.AddControllers().AddJsonOptions(options => 394 | options.JsonSerializerOptions.Converters.Add(new ErrorDtoSerializer(httpContextAccessor))); 395 | ```` 396 | But alternatively you can use the following one: 397 | ````csharp 398 | services.AddControllers().AddJsonOptions(options => 399 | { 400 | options.JsonSerializerOptions.AddTranslation(services); 401 | } ); 402 | ```` 403 | 404 | 405 | You can find more information in [My blog *in spanish*](https://www.netmentor.es/entrada/custom-json-serializer). 406 | 407 | ## Tuples 408 | [Tuples](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples) in C# are a powerful type that allows you to return more than one value in a single response. 409 | 410 | They are very useful for us when developing using ROP. because It allows us to query a value and propagate the input with the new value. for example in the next scenario: 411 | ````csharp 412 | Result result = ValidateNewAccount(account) 413 | .Bind(SaveUser) 414 | .Bind(GetManagerEmail) //<- new method 415 | .Bind(SendCode); 416 | ```` 417 | Here, we see a new method called `GetManagerEmail` which basically will check the user ManagerID and get its email; this could be useful to specify in the `sendCode` an email to contact if something does not work. 418 | 419 | The implementation of `GetManagerEmail` will look like the next. 420 | `````csharp 421 | private async Task> GetManagerEmail(Account account){ 422 | string managerEmail = await _dependencies.GetManagerEmail(account.ManagerId); 423 | return (account.Email, managerEmail) 424 | } 425 | ````` 426 | As you can see, we are returning a tuple into the method, which converts automatically into `Result` being `T` the tuple `(Account, string)`. 427 | 428 | Then in the following method, you have to receive the tuple. Tuples are a type, not two different types, so that they will be together. There is nothing mandatory here, but normally is easy to identify if you call them in the parameters as `args` 429 | `````csharp 430 | //This will be the implementation of the third method. 431 | private async Task> SendCode((string, string) args) 432 | { 433 | return await _dependencies.SendEmailWithManagerReference(args.Item1, args.Item2); 434 | } 435 | ````` 436 | You also have the option to name the tuples `SendCode((string UserEmail, string ManagerEmail) args)`, and then you will access with `args.UserEmail` and `args.ManagerEmail`. 437 | 438 | 439 | 440 | ## Issues and contributing 441 | 442 | Please do not hesitate in adding some issue or contribute in the code. 443 | 444 | -------------------------------------------------------------------------------- /ROP.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32519.111 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ROP", "src\ROP\ROP.csproj", "{6069CC5A-78CE-4A04-A6DD-DD5CAF17F073}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ROP.Ejemplo.CasoDeUso", "test\ROP.Ejemplo.CasoDeUso\ROP.Ejemplo.CasoDeUso.csproj", "{C9A22175-74E6-4909-ADED-7493CCC2B552}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ROP.UnitTest", "test\ROP.UnitTest\ROP.UnitTest.csproj", "{357A2DA5-9156-48C7-8B48-8746BB4D3220}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ROP.ApiExtensions", "src\ROP.ApiExtensions\ROP.ApiExtensions.csproj", "{0A3923EF-BAE3-4705-81AD-C330DE33EFFD}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ROP.ApiExtensions.Translations", "src\ROP.ApiExtensions.Translations\ROP.ApiExtensions.Translations.csproj", "{F6218AB5-F050-42B5-8849-5B58C0B414B3}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elementos de la solución", "Elementos de la solución", "{D107F4C8-89CC-BEA8-D3D7-CA6B7733353D}" 17 | ProjectSection(SolutionItems) = preProject 18 | bump-config.json = bump-config.json 19 | EndProjectSection 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {6069CC5A-78CE-4A04-A6DD-DD5CAF17F073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {6069CC5A-78CE-4A04-A6DD-DD5CAF17F073}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {6069CC5A-78CE-4A04-A6DD-DD5CAF17F073}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {6069CC5A-78CE-4A04-A6DD-DD5CAF17F073}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {C9A22175-74E6-4909-ADED-7493CCC2B552}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {C9A22175-74E6-4909-ADED-7493CCC2B552}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {C9A22175-74E6-4909-ADED-7493CCC2B552}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {C9A22175-74E6-4909-ADED-7493CCC2B552}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {357A2DA5-9156-48C7-8B48-8746BB4D3220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {357A2DA5-9156-48C7-8B48-8746BB4D3220}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {357A2DA5-9156-48C7-8B48-8746BB4D3220}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {357A2DA5-9156-48C7-8B48-8746BB4D3220}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {0A3923EF-BAE3-4705-81AD-C330DE33EFFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {0A3923EF-BAE3-4705-81AD-C330DE33EFFD}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {0A3923EF-BAE3-4705-81AD-C330DE33EFFD}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {0A3923EF-BAE3-4705-81AD-C330DE33EFFD}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {F6218AB5-F050-42B5-8849-5B58C0B414B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {F6218AB5-F050-42B5-8849-5B58C0B414B3}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {F6218AB5-F050-42B5-8849-5B58C0B414B3}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {F6218AB5-F050-42B5-8849-5B58C0B414B3}.Release|Any CPU.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | GlobalSection(ExtensibilityGlobals) = postSolution 52 | SolutionGuid = {C58F84BB-3AED-43E7-A63A-29C4E96612EB} 53 | EndGlobalSection 54 | EndGlobal 55 | -------------------------------------------------------------------------------- /bump-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "isMajor": false, 3 | "isMinor": true, 4 | "isPatch": false 5 | } 6 | -------------------------------------------------------------------------------- /src/ROP.ApiExtensions.Translations/Language/CultureScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading; 4 | 5 | namespace ROP.ApiExtensions.Translations.Language 6 | { 7 | /// 8 | /// Represents a scope in which the current culture and UI culture are set to a specific . 9 | /// 10 | public class CultureScope : IDisposable 11 | { 12 | private readonly CultureInfo _originalCulture; 13 | private readonly CultureInfo _originalUICulture; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// 19 | public CultureScope(CultureInfo culture) 20 | { 21 | _originalCulture = Thread.CurrentThread.CurrentCulture; 22 | _originalUICulture = Thread.CurrentThread.CurrentUICulture; 23 | 24 | Thread.CurrentThread.CurrentCulture = culture; 25 | Thread.CurrentThread.CurrentUICulture = culture; 26 | } 27 | 28 | /// 29 | /// Restores the original culture and UI culture. 30 | /// 31 | public void Dispose() 32 | { 33 | Thread.CurrentThread.CurrentCulture = _originalCulture; 34 | Thread.CurrentThread.CurrentUICulture = _originalUICulture; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/ROP.ApiExtensions.Translations/Language/Extensions/AcceptedLanguageExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace ROP.ApiExtensions.Translations.Language.Extensions 8 | { 9 | /// 10 | /// Extension to get the language from the header Accept-Language. 11 | /// 12 | public static class AcceptedLanguageExtension 13 | { 14 | /// 15 | /// Pick the favorite supported Language by the user browser. 16 | /// if that language does not exist, the translation will pick the default one, English. 17 | /// based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language 18 | /// the header always follow the next pattern "ES;q=0.1", doesn't matter which country is the browser from. 19 | /// Need to set up the scope to a country which the decimals are dots. 20 | /// 21 | /// 22 | /// 23 | public static CultureInfo GetCultureInfo(this IHeaderDictionary header) 24 | { 25 | using (new CultureScope(new CultureInfo("es"))) 26 | { 27 | var languages = new List<(string, decimal)>(); 28 | string acceptedLanguage = header["Accept-Language"]; 29 | if (string.IsNullOrEmpty(acceptedLanguage)) 30 | { 31 | return new CultureInfo("es"); 32 | } 33 | string[] acceptedLanguages = acceptedLanguage.Split(','); 34 | foreach (string accLang in acceptedLanguages) 35 | { 36 | var languageDetails = accLang.Split(';'); 37 | if (languageDetails.Length == 1) 38 | { 39 | languages.Add((languageDetails[0], 1)); 40 | } 41 | else 42 | { 43 | languages.Add((languageDetails[0], Convert.ToDecimal(languageDetails[1].Replace("q=", "")))); 44 | } 45 | } 46 | string languageToSet = languages.OrderByDescending(a => a.Item2).First().Item1; 47 | return new CultureInfo(languageToSet); 48 | } 49 | } 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /src/ROP.ApiExtensions.Translations/Language/LocalizationUtils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Localization; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | using Microsoft.Extensions.Options; 4 | using System.Globalization; 5 | 6 | namespace ROP.ApiExtensions.Translations.Language 7 | { 8 | /// 9 | /// Utility class to get the localized value of an entity. 10 | /// 11 | /// 12 | public class LocalizationUtils 13 | { 14 | 15 | private static readonly IStringLocalizer _localizer; 16 | 17 | static LocalizationUtils() 18 | { 19 | var options = Options.Create(new LocalizationOptions()); 20 | var factory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance); 21 | var type = typeof(TEntity); 22 | 23 | _localizer = factory.Create(type); 24 | } 25 | 26 | /// 27 | /// Gets the localized value of a field. 28 | /// 29 | /// 30 | /// 31 | public static string GetValue(string field) 32 | { 33 | return _localizer[field]; 34 | } 35 | 36 | /// 37 | /// Gets the localized value of a field in a specific culture. 38 | /// 39 | /// 40 | /// 41 | /// 42 | public static string GetValue(string field, CultureInfo cultureInfo) 43 | { 44 | using (new CultureScope(cultureInfo)) 45 | { 46 | return GetValue(field); 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/ROP.ApiExtensions.Translations/ROP.ApiExtensions.Translations.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;netstandard2.1 4 | true 5 | true 6 | Netmentor.ROP.ApiExtensions.Translations 7 | 1.9.0 8 | Ivan Abad 9 | NetMentor 10 | Extension library created for RoP to handle translations 11 | MIT 12 | https://github.com/ElectNewt/EjemploRop 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/ROP.ApiExtensions.Translations/RopTranslationsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using ROP.ApiExtensions.Translations.Serializers; 6 | 7 | namespace ROP.ApiExtensions.Translations 8 | { 9 | /// 10 | /// Extensions to add translations to the JsonSerializerOptions. 11 | /// 12 | public static class RopTranslationsExtensions 13 | { 14 | /// 15 | /// Allows to detect automatic translations (with resx files) in case you have more than one language supported 16 | /// please see https://github.com/ElectNewt/EjemploRop#error-translations for mor details; usage: 17 | /// services.AddControllers().AddJsonOptions(options =>{options.JsonSerializerOptions.AddTranslation<TraduccionErrores>(services);} ); 18 | /// it uses the "Accept-Language" header to define the selected language. 19 | /// 20 | /// Serializer options 21 | /// IHttpContextAccessor 22 | /// Translation class that 'references' the .resx files 23 | public static void AddTranslation(this JsonSerializerOptions options, 24 | IHttpContextAccessor httpContextAccessor) 25 | { 26 | options.Converters.Add(BuildErrorDtoSerializer(httpContextAccessor)); 27 | } 28 | 29 | /// 30 | /// Allows to detect automatic translations (with resx files) in case you have more than one language supported 31 | /// please see https://github.com/ElectNewt/EjemploRop#error-translations for mor details; usage: 32 | /// services.AddControllers().AddJsonOptions(options =>{options.JsonSerializerOptions.AddTranslation<TraduccionErrores>(services);} ); 33 | /// it uses the "Accept-Language" header to define the selected language. 34 | /// 35 | /// Serializer options 36 | /// Service collection 37 | /// Translation class that 'references' the .resx files 38 | public static void AddTranslation(this JsonSerializerOptions options, 39 | IServiceCollection services) 40 | { 41 | IServiceProvider serviceProvider = services.BuildServiceProvider(); 42 | options.AddTranslation(serviceProvider); 43 | } 44 | 45 | /// 46 | /// Allows to detect automatic translations (with resx files) in case you have more than one language supported 47 | /// please see https://github.com/ElectNewt/EjemploRop#error-translations for mor details; usage: 48 | /// services.AddControllers().AddJsonOptions(options =>{options.JsonSerializerOptions.AddTranslation<TraduccionErrores>(services);} ); 49 | /// it uses the "Accept-Language" header to define the selected language. 50 | /// 51 | /// Serializer options 52 | /// Service Provider 53 | /// Translation class that 'references' the .resx files 54 | public static void AddTranslation(this JsonSerializerOptions options, 55 | IServiceProvider serviceProvider) 56 | { 57 | IHttpContextAccessor httpContextAccessor = serviceProvider.GetService(); 58 | options.Converters.Add(BuildErrorDtoSerializer(httpContextAccessor)); 59 | } 60 | 61 | private static ErrorDtoSerializer BuildErrorDtoSerializer( 62 | IHttpContextAccessor httpContextAccessor) 63 | { 64 | return new ErrorDtoSerializer(httpContextAccessor); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/ROP.ApiExtensions.Translations/Serializers/ErrorDtoSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using ROP.APIExtensions; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | using Microsoft.AspNetCore.Http; 7 | using ROP.ApiExtensions.Translations.Language; 8 | using ROP.ApiExtensions.Translations.Language.Extensions; 9 | 10 | namespace ROP.ApiExtensions.Translations.Serializers 11 | { 12 | /// 13 | /// Serializer for the ErrorDto, it will use the translation file to get the error message. 14 | /// 15 | /// 16 | public class ErrorDtoSerializer : JsonConverter 17 | { 18 | private readonly IHttpContextAccessor _httpContextAccessor; 19 | 20 | /// 21 | /// Constructor for the ErrorDtoSerializer. 22 | /// 23 | /// 24 | public ErrorDtoSerializer(IHttpContextAccessor httpContextAccessor) 25 | { 26 | _httpContextAccessor = httpContextAccessor; 27 | } 28 | 29 | /// 30 | /// Reads the error from the json and returns the ErrorDto. 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | public override ErrorDto Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 39 | { 40 | string errorMessage = null; 41 | string[] translationVariables = null; 42 | Guid errorCode = Guid.NewGuid(); 43 | 44 | while (reader.Read()) 45 | { 46 | if (reader.TokenType == JsonTokenType.EndObject) 47 | { 48 | break; 49 | } 50 | 51 | if (reader.TokenType != JsonTokenType.PropertyName) 52 | { 53 | throw new JsonException(); 54 | } 55 | 56 | string propertyName = reader.GetString(); 57 | 58 | if (propertyName == nameof(ErrorDto.ErrorCode)) 59 | { 60 | reader.Read(); 61 | errorCode = Guid.Parse(reader.GetString() ?? string.Empty); 62 | } 63 | 64 | if (propertyName == nameof(ErrorDto.Message)) 65 | { 66 | reader.Read(); 67 | errorMessage = reader.GetString(); 68 | } 69 | 70 | if (propertyName == nameof(Error.TranslationVariables)) 71 | { 72 | using (var jsonDoc = JsonDocument.ParseValue(ref reader)) 73 | { 74 | var result = jsonDoc.RootElement.GetRawText(); 75 | translationVariables = JsonSerializer.Deserialize(result); 76 | } 77 | } 78 | 79 | } 80 | 81 | //theoretically with the translation in place errormessage will never be null 82 | if (errorMessage == null) 83 | throw new Exception("Either Message or the ErrorCode has to be populated into the error"); 84 | 85 | return new ErrorDto() 86 | { 87 | ErrorCode = errorCode, 88 | Message = String.Format(errorMessage, translationVariables ?? new string[0]) 89 | }; 90 | } 91 | 92 | /// 93 | /// Writes the error to the json. 94 | /// 95 | /// 96 | /// 97 | /// 98 | public override void Write(Utf8JsonWriter writer, ErrorDto value, JsonSerializerOptions options) 99 | { 100 | string errorMessageValue = value.Message; 101 | if (string.IsNullOrWhiteSpace(value.Message)) 102 | { 103 | CultureInfo language = _httpContextAccessor.HttpContext.Request.Headers.GetCultureInfo(); 104 | errorMessageValue = LocalizationUtils.GetValue(value.ErrorCode.ToString(), language); 105 | 106 | if (value.TranslationVariables != null) 107 | { 108 | errorMessageValue = string.Format(errorMessageValue, (string[])value.TranslationVariables); 109 | } 110 | } 111 | 112 | writer.WriteStartObject(); 113 | writer.WriteString(nameof(Error.ErrorCode), value.ErrorCode.ToString()); 114 | writer.WriteString(nameof(Error.Message), errorMessageValue); 115 | writer.WritePropertyName(nameof(Error.TranslationVariables)); 116 | JsonSerializer.Serialize(writer, value.TranslationVariables ?? Array.Empty(), options); 117 | writer.WriteEndObject(); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/ROP.ApiExtensions/ActionResultExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | 7 | namespace ROP.APIExtensions 8 | { 9 | /// 10 | /// Provides extension methods to convert results into IActionResult. 11 | /// 12 | public static class ActionResultExtensions 13 | { 14 | /// 15 | /// Converts a result T chain, into an IActionResult, it uses the HttpStatusCode on the result chain 16 | /// use .UseSuccessHttpStatusCode(HttpStatusCode) if you want to set it up. 17 | /// 18 | /// The type of the value in the result. 19 | /// The result to convert. 20 | /// Returns a ResultDto of T. If you want to return T or ProblemDetails in case of an error, use ToResultOrProblemDetails. 21 | public static IActionResult ToActionResult(this Result result) 22 | { 23 | return result.ToDto().ToHttpStatusCode(result.HttpStatusCode); 24 | } 25 | 26 | /// 27 | /// Converts a result T chain, into an IActionResult, it uses the HttpStatusCode on the result chain 28 | /// use .UseSuccessHttpStatusCode(HttpStatusCode) if you want to set it up. 29 | /// 30 | /// The type of the value in the result. 31 | /// The result to convert. 32 | /// Returns a ResultDto of T. If you want to return T or ProblemDetails in case of an error, use ToResultOrProblemDetails. 33 | public static async Task ToActionResult(this Task> result) 34 | { 35 | Result r = await result; 36 | 37 | return r.ToActionResult(); 38 | } 39 | 40 | 41 | /// 42 | /// Converts a result T chain, into an IActionResult with the value or ProblemDetails. 43 | /// It uses the HttpStatusCode on the result chain. use .UseSuccessHttpStatusCode(HttpStatusCode) if you want to set it up. 44 | /// 45 | /// Returns the value or a ProblemDetails in case of an error. 46 | public static IActionResult ToValueOrProblemDetails(this Result result) 47 | { 48 | if (result.Success) 49 | { 50 | return result.Value.ToHttpStatusCode(result.HttpStatusCode); 51 | } 52 | 53 | ProblemDetails problemDetails = new ProblemDetails() 54 | { 55 | Title = "Error(s) found", 56 | Status = (int)result.HttpStatusCode, 57 | Detail = "One or more errors occurred", 58 | }; 59 | 60 | problemDetails.Extensions.Add("ValidationErrors", result.Errors.Select(x => x.ToErrorDto()).ToList()); 61 | 62 | return problemDetails.ToHttpStatusCode(result.HttpStatusCode); 63 | } 64 | 65 | /// 66 | /// Converts a result T chain, into an IActionResult with the value or ProblemDetails. 67 | /// It uses the HttpStatusCode on the result chain. use .UseSuccessHttpStatusCode(HttpStatusCode) if you want to set it up. 68 | /// 69 | /// Returns the value or a ProblemDetails in case of an error. 70 | public static async Task ToValueOrProblemDetails(this Task> result) 71 | { 72 | Result r = await result; 73 | 74 | return r.ToValueOrProblemDetails(); 75 | } 76 | 77 | private static IActionResult ToHttpStatusCode(this T resultDto, HttpStatusCode statusCode) 78 | { 79 | return new ResultWithStatusCode(resultDto, statusCode); 80 | } 81 | 82 | private static ResultDto ToDto(this Result result) 83 | { 84 | if (result.Success) 85 | return new ResultDto() 86 | { 87 | Value = result.Value, 88 | Errors = ImmutableArray.Empty 89 | }; 90 | 91 | return new ResultDto() 92 | { 93 | Value = default, 94 | Errors = result.Errors.Select(x => x.ToErrorDto()).ToImmutableArray() 95 | }; 96 | } 97 | 98 | 99 | private class ResultWithStatusCode : ObjectResult 100 | { 101 | public ResultWithStatusCode(T content, HttpStatusCode httpStatusCode) 102 | : base(content) 103 | { 104 | StatusCode = (int)httpStatusCode; 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /src/ROP.ApiExtensions/ErrorDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ROP.APIExtensions 4 | { 5 | /// 6 | /// Represents a Data Transfer Object (DTO) for an error, containing a message, an optional error code, and translation variables. 7 | /// 8 | public class ErrorDto 9 | { 10 | /// 11 | /// Gets or sets the error message. 12 | /// 13 | public string Message { get; set; } 14 | 15 | /// 16 | /// Gets or sets the optional error code. 17 | /// 18 | public Guid? ErrorCode { get; set; } 19 | 20 | /// 21 | /// Gets or sets the variables used for translating the error message. 22 | /// 23 | public string[] TranslationVariables { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ROP.ApiExtensions/ErrorDtoExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ROP.APIExtensions 2 | { 3 | internal static class ErrorDtoExtensions 4 | { 5 | internal static ErrorDto ToErrorDto(this Error error) 6 | => new ErrorDto() 7 | { 8 | ErrorCode = error.ErrorCode, 9 | Message = error.Message, 10 | TranslationVariables = error.TranslationVariables 11 | }; 12 | } 13 | } -------------------------------------------------------------------------------- /src/ROP.ApiExtensions/ROP.ApiExtensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;netstandard2.1 4 | true 5 | true 6 | Netmentor.ROP.ApiExtensions 7 | 1.9.0 8 | Ivan Abad 9 | NetMentor 10 | Extension library related to API MVC for Netmentor.ROP 11 | MIT 12 | https://github.com/ElectNewt/EjemploRop 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ROP.ApiExtensions/ResultDto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace ROP.APIExtensions 4 | { 5 | /// 6 | /// Represents a Data Transfer Object (DTO) for a result, containing a value and a collection of errors. 7 | /// 8 | /// The type of the value contained in the result. 9 | public class ResultDto 10 | { 11 | /// 12 | /// Gets or sets the value of the result. 13 | /// 14 | public T Value { get; set; } 15 | 16 | /// 17 | /// Gets or sets the collection of errors associated with the result. 18 | /// 19 | public ImmutableArray Errors { get; set; } 20 | 21 | /// 22 | /// Gets a value indicating whether the result is successful. 23 | /// 24 | /// True if there are no errors; otherwise, false. 25 | public bool Success => Errors.Length == 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ROP/Error.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ROP 5 | { 6 | /// 7 | /// Represents an error that occurred during the execution of a Result. 8 | /// 9 | public class Error 10 | { 11 | /// 12 | /// The error message. 13 | /// 14 | public readonly string Message; 15 | 16 | /// 17 | /// The error code. 18 | /// 19 | public readonly Guid? ErrorCode; 20 | 21 | /// 22 | /// The variables used for translating the error message. 23 | /// 24 | public readonly string[] TranslationVariables; 25 | 26 | private Error(string message, Guid? errorCode, string[] translationVariables) 27 | { 28 | Message = message; 29 | ErrorCode = errorCode; 30 | TranslationVariables = translationVariables; 31 | } 32 | 33 | /// 34 | /// Creates a new error with a static message. Prefer Create override with the error code for automatic translations 35 | /// 36 | /// static message 37 | /// Guid specifying the error code 38 | /// if your error message uses variables in the translation, you can specify them here 39 | public static Error Create(string message, Guid? errorCode = null, string[] translationVariables = null) 40 | { 41 | return new Error(message, errorCode, translationVariables); 42 | } 43 | 44 | /// 45 | /// Creates a new error with an error code that can be used to resolve translated error messages. Prefer using this method. 46 | /// Check the docs for info on translations. 47 | /// 48 | /// Guid specifying the error code 49 | /// if your error message uses variables in the translation, you can specify them here 50 | public static Error Create(Guid errorCode, string[] translationVariables = null) 51 | { 52 | return Error.Create(string.Empty, errorCode, translationVariables); 53 | } 54 | 55 | /// 56 | /// Converts an exception into a collection of Error objects. 57 | /// 58 | /// 59 | /// 60 | public static IEnumerable Exception(Exception e) 61 | { 62 | if (e is ErrorResultException errs) 63 | { 64 | return errs.Errors; 65 | } 66 | 67 | return new[] 68 | { 69 | Create(e.ToString()) 70 | }; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ROP/ErrorResultException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | 5 | namespace ROP 6 | { 7 | /// 8 | /// Custom exception that encapsulates the errors 9 | /// 10 | public class ErrorResultException : Exception 11 | { 12 | /// 13 | /// The errors that occurred. 14 | /// 15 | public ImmutableArray Errors { get; } 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// 21 | public ErrorResultException(ImmutableArray errors) 22 | : base(ValidateAndGetErrorMessage(errors)) 23 | { 24 | Errors = errors; 25 | } 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// 31 | public ErrorResultException(Error error) 32 | : this(new[] { error }.ToImmutableArray()) 33 | { 34 | } 35 | 36 | private static string ValidateAndGetErrorMessage(ImmutableArray errors) 37 | { 38 | if (errors.Length == 0) 39 | { 40 | throw new Exception("You should include at least one Error"); 41 | } 42 | 43 | if (errors.Length == 1) 44 | { 45 | return errors[0].Message; 46 | } 47 | 48 | return errors 49 | .Select(e => e.Message) 50 | .Prepend($"{errors.Length} Errors occurred:") 51 | .JoinStrings(System.Environment.NewLine); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ROP/IEnumerableUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ROP 4 | { 5 | /// 6 | /// Extension methods for IEnumerable. 7 | /// 8 | public static class IEnumerableUtils 9 | { 10 | /// 11 | /// Shorthand for string.Join(separator, strings) 12 | /// 13 | public static string JoinStrings(this IEnumerable strings, string separator) 14 | { 15 | return string.Join(separator, strings); 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ROP/ROP.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | Netmentor.ROP 6 | 1.9.0 7 | Ivan Abad 8 | NetMentor 9 | Library to handle errors in C# in a more functional way in Railway oriented programming. based on Scott Wlaschin's Idea. 10 | MIT 11 | https://github.com/ElectNewt/EjemploRop 12 | net8.0;netstandard2.1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/ROP/Result.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading.Tasks; 3 | 4 | namespace ROP 5 | { 6 | /// 7 | /// Provides extension methods to create Result objects. 8 | /// 9 | public static partial class Result 10 | { 11 | /// 12 | /// Object to avoid using void 13 | /// 14 | public static readonly Unit Unit = Unit.Value; 15 | 16 | /// 17 | /// Chains an object into the Result Structure 18 | /// 19 | public static Result Success(this T value) => new Result(value, HttpStatusCode.OK); 20 | 21 | /// 22 | /// Chains an object into the Result Structure 23 | /// 24 | public static Result Success(this T value, HttpStatusCode httpStatusCode) => new Result(value, httpStatusCode); 25 | 26 | /// 27 | /// Chains an Result.Unit into the Result Structure 28 | /// 29 | public static Result Success() => new Result(Unit, HttpStatusCode.OK); 30 | 31 | /// 32 | /// Converts a synchronous Result structure into async 33 | /// 34 | public static Task> Async(this Result r) => Task.FromResult(r); 35 | 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ROP/ResultFailure/Result_BadRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Net; 6 | 7 | namespace ROP 8 | { 9 | public static partial class Result 10 | { 11 | /// 12 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 13 | /// 14 | public static Result BadRequest(ImmutableArray errors) => new Result(errors, HttpStatusCode.BadRequest); 15 | 16 | /// 17 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 18 | /// 19 | public static Result BadRequest(Error error) => new Result(ImmutableArray.Create(error), HttpStatusCode.BadRequest); 20 | 21 | /// 22 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 23 | /// 24 | public static Result BadRequest(string error) => new Result(ImmutableArray.Create(Error.Create(error)), HttpStatusCode.BadRequest); 25 | 26 | /// 27 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 28 | /// 29 | public static Result BadRequest(Guid errorCode) => BadRequest(Error.Create(errorCode)); 30 | 31 | /// 32 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 33 | /// 34 | public static Result BadRequest(ImmutableArray errors) => new Result(errors, HttpStatusCode.BadRequest); 35 | 36 | /// 37 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 38 | /// 39 | public static Result BadRequest(IEnumerable errors) => 40 | new Result(ImmutableArray.Create(errors.ToArray()), HttpStatusCode.BadRequest); 41 | 42 | /// 43 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 44 | /// 45 | public static Result BadRequest(Error error) => new Result(ImmutableArray.Create(error), HttpStatusCode.BadRequest); 46 | 47 | /// 48 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 49 | /// 50 | public static Result BadRequest(string error) => 51 | new Result(ImmutableArray.Create(Error.Create(error)), HttpStatusCode.BadRequest); 52 | 53 | /// 54 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 55 | /// 56 | public static Result BadRequest(Guid errorCode, string[] translationVariables = null) => 57 | BadRequest(Error.Create(errorCode, translationVariables)); 58 | } 59 | } -------------------------------------------------------------------------------- /src/ROP/ResultFailure/Result_Conflict.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Net; 6 | 7 | namespace ROP 8 | { 9 | public static partial class Result 10 | { 11 | /// 12 | /// Converts the type into the error flow with HttpStatusCode.Conflict 13 | /// 14 | public static Result Conflict(ImmutableArray errors) => new Result(errors, HttpStatusCode.Conflict); 15 | 16 | /// 17 | /// Converts the type into the error flow with HttpStatusCode.Conflict 18 | /// 19 | public static Result Conflict(Error error) => new Result(ImmutableArray.Create(error), HttpStatusCode.Conflict); 20 | 21 | /// 22 | /// Converts the type into the error flow with HttpStatusCode.Conflict 23 | /// 24 | public static Result Conflict(string error) => new Result(ImmutableArray.Create(Error.Create(error)), HttpStatusCode.Conflict); 25 | 26 | /// 27 | /// Converts the type into the error flow with HttpStatusCode.Conflict 28 | /// 29 | public static Result Conflict(Guid errorCode) => Conflict(Error.Create(errorCode)); 30 | 31 | /// 32 | /// Converts the type into the error flow with HttpStatusCode.Conflict 33 | /// 34 | public static Result Conflict(ImmutableArray errors) => new Result(errors, HttpStatusCode.Conflict); 35 | 36 | /// 37 | /// Converts the type into the error flow with HttpStatusCode.Conflict 38 | /// 39 | public static Result Conflict(IEnumerable errors) => new Result(ImmutableArray.Create(errors.ToArray()), HttpStatusCode.Conflict); 40 | 41 | /// 42 | /// Converts the type into the error flow with HttpStatusCode.Conflict 43 | /// 44 | public static Result Conflict(Error error) => new Result(ImmutableArray.Create(error), HttpStatusCode.Conflict); 45 | 46 | /// 47 | /// Converts the type into the error flow with HttpStatusCode.Conflict 48 | /// 49 | public static Result Conflict(string error) => new Result(ImmutableArray.Create(Error.Create(error)), HttpStatusCode.Conflict); 50 | 51 | /// 52 | /// Converts the type into the error flow with HttpStatusCode.Conflict 53 | /// 54 | public static Result Conflict(Guid errorCode, string[] translationVariables = null) => Conflict(Error.Create(errorCode, translationVariables)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ROP/ResultFailure/Result_Failure.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Net; 6 | 7 | namespace ROP 8 | { 9 | public static partial class Result 10 | { 11 | /// 12 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 13 | /// 14 | public static Result Failure(ImmutableArray errors) => new Result(errors, HttpStatusCode.BadRequest); 15 | 16 | /// 17 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 18 | /// 19 | public static Result Failure(ImmutableArray errors, HttpStatusCode httpStatusCode) => new Result(errors, httpStatusCode); 20 | 21 | /// 22 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 23 | /// 24 | public static Result Failure(Error error) => new Result(ImmutableArray.Create(error), HttpStatusCode.BadRequest); 25 | 26 | /// 27 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 28 | /// 29 | public static Result Failure(string error) => new Result(ImmutableArray.Create(Error.Create(error)), HttpStatusCode.BadRequest); 30 | 31 | /// 32 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 33 | /// 34 | public static Result Failure(Guid errorCode) => Failure(Error.Create(errorCode)); 35 | 36 | /// 37 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 38 | /// 39 | public static Result Failure(ImmutableArray errors) => new Result(errors, HttpStatusCode.BadRequest); 40 | 41 | /// 42 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 43 | /// 44 | public static Result Failure(ImmutableArray errors, HttpStatusCode httpStatusCode) => new Result(errors, httpStatusCode); 45 | 46 | /// 47 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 48 | /// 49 | public static Result Failure(IEnumerable errors) => 50 | new Result(ImmutableArray.Create(errors.ToArray()), HttpStatusCode.BadRequest); 51 | 52 | /// 53 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 54 | /// 55 | public static Result Failure(Error error) => new Result(ImmutableArray.Create(error), HttpStatusCode.BadRequest); 56 | 57 | /// 58 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 59 | /// 60 | public static Result Failure(string error) => new Result(ImmutableArray.Create(Error.Create(error)), HttpStatusCode.BadRequest); 61 | 62 | /// 63 | /// Converts the type into the error flow with HttpStatusCode.BadRequest 64 | /// 65 | public static Result Failure(Guid errorCode, string[] translationVariables = null) => 66 | Failure(Error.Create(errorCode, translationVariables)); 67 | } 68 | } -------------------------------------------------------------------------------- /src/ROP/ResultFailure/Result_NotFound.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Net; 6 | 7 | namespace ROP 8 | { 9 | public static partial class Result 10 | { 11 | /// 12 | /// Converts the type into the error flow with HttpStatusCode.NotFound 13 | /// 14 | public static Result NotFound(ImmutableArray errors) => new Result(errors, HttpStatusCode.NotFound); 15 | 16 | /// 17 | /// Converts the type into the error flow with HttpStatusCode.NotFound 18 | /// 19 | public static Result NotFound(Error error) => new Result(ImmutableArray.Create(error), HttpStatusCode.NotFound); 20 | 21 | /// 22 | /// Converts the type into the error flow with HttpStatusCode.NotFound 23 | /// 24 | public static Result NotFound(string error) => new Result(ImmutableArray.Create(Error.Create(error)), HttpStatusCode.NotFound); 25 | 26 | /// 27 | /// Converts the type into the error flow with HttpStatusCode.NotFound 28 | /// 29 | public static Result NotFound(Guid errorCode) => NotFound(Error.Create(errorCode)); 30 | 31 | /// 32 | /// Converts the type into the error flow with HttpStatusCode.NotFound 33 | /// 34 | public static Result NotFound(ImmutableArray errors) => new Result(errors, HttpStatusCode.NotFound); 35 | 36 | /// 37 | /// Converts the type into the error flow with HttpStatusCode.NotFound 38 | /// 39 | public static Result NotFound(IEnumerable errors) => 40 | new Result(ImmutableArray.Create(errors.ToArray()), HttpStatusCode.NotFound); 41 | 42 | /// 43 | /// Converts the type into the error flow with HttpStatusCode.NotFound 44 | /// 45 | public static Result NotFound(Error error) => new Result(ImmutableArray.Create(error), HttpStatusCode.NotFound); 46 | 47 | /// 48 | /// Converts the type into the error flow with HttpStatusCode.NotFound 49 | /// 50 | public static Result NotFound(string error) => new Result(ImmutableArray.Create(Error.Create(error)), HttpStatusCode.NotFound); 51 | 52 | /// 53 | /// Converts the type into the error flow with HttpStatusCode.NotFound 54 | /// 55 | public static Result NotFound(Guid errorCode, string[] translationVariables = null) => 56 | NotFound(Error.Create(errorCode, translationVariables)); 57 | } 58 | } -------------------------------------------------------------------------------- /src/ROP/Result_Bind.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace ROP 6 | { 7 | /// 8 | /// Provides extension methods to chain two methods, the output of the first is the input of the second. 9 | /// 10 | public static class Result_Bind 11 | { 12 | 13 | /// 14 | /// Allows to chain two methods, the output of the first is the input of the second. 15 | /// 16 | /// current Result chain 17 | /// method to execute 18 | /// Input type 19 | /// Output type 20 | /// Result Structure of the return type 21 | public static Result Bind(this Result r, Func> method) 22 | { 23 | try 24 | { 25 | return r.Success 26 | ? method(r.Value) 27 | : Result.Failure(r.Errors, r.HttpStatusCode); 28 | } 29 | catch (Exception e) 30 | { 31 | ExceptionDispatchInfo.Capture(e).Throw(); 32 | throw; 33 | } 34 | } 35 | 36 | /// 37 | /// Allows to chain a non async method to an async method, the output of the first is the input of the second. 38 | /// 39 | /// current Result chain 40 | /// method to execute 41 | /// Input type 42 | /// Output type 43 | /// Async Result Structure of the return type 44 | public static async Task> Bind(this Result r, Func>> method) 45 | { 46 | try 47 | { 48 | return r.Success 49 | ? await method(r.Value) 50 | : Result.Failure(r.Errors, r.HttpStatusCode); 51 | } 52 | catch (Exception e) 53 | { 54 | ExceptionDispatchInfo.Capture(e).Throw(); 55 | throw; 56 | } 57 | } 58 | 59 | /// 60 | /// Allows to chain two async methods, the output of the first is the input of the second. 61 | /// 62 | /// current Result chain 63 | /// method to execute 64 | /// Input type 65 | /// Output type 66 | /// Async Result Structure of the return type 67 | public static async Task> Bind(this Task> result, Func>> method) 68 | { 69 | try 70 | { 71 | var r = await result; 72 | return await r.Bind(method); 73 | } 74 | catch (Exception e) 75 | { 76 | ExceptionDispatchInfo.Capture(e).Throw(); 77 | throw; 78 | } 79 | } 80 | 81 | /// 82 | /// Allows to chain an async method to a non async method, the output of the first is the input of the second. 83 | /// 84 | /// current Result chain 85 | /// method to execute 86 | /// Input type 87 | /// Output type 88 | /// Async Result Structure of the return type 89 | public static async Task> Bind(this Task> result, Func> method) 90 | { 91 | try 92 | { 93 | var r = await result; 94 | return r.Bind(method); 95 | } 96 | catch (Exception e) 97 | { 98 | ExceptionDispatchInfo.Capture(e).Throw(); 99 | throw; 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/ROP/Result_Combine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace ROP 6 | { 7 | /// 8 | /// Provides extension methods to combine the result of two methods, the input of the combined method is the result of the first method. 9 | /// 10 | public static class Result_Combine 11 | { 12 | /// 13 | /// Allows to combine the result of two methods. the input of the combined method is the result of the first method. 14 | /// 15 | /// A result chain that contains a tuple with both results 16 | public static Result<(T1, T2)> Combine(this Result r, Func> action) 17 | { 18 | try 19 | { 20 | return r.Bind(action) 21 | .Map(x => (r.Value, x)); 22 | } 23 | catch (Exception e) 24 | { 25 | ExceptionDispatchInfo.Capture(e).Throw(); 26 | throw; 27 | } 28 | } 29 | 30 | /// 31 | /// Allows to combine the result of two methods. the input of the combined method is the result of the first method. 32 | /// 33 | /// A result chain that contains a tuple with both results 34 | public static async Task> Combine(this Result r, Func>> action) 35 | { 36 | try 37 | { 38 | return await r.Bind(action) 39 | .Map(x => (r.Value, x)); 40 | } 41 | catch (Exception e) 42 | { 43 | ExceptionDispatchInfo.Capture(e).Throw(); 44 | throw; 45 | } 46 | } 47 | 48 | /// 49 | /// Allows to combine the result of two methods. the input of the combined method is the result of the first method. 50 | /// 51 | /// A result chain that contains a tuple with both results 52 | public static async Task> Combine(this Task> result, Func>> action) 53 | { 54 | try 55 | { 56 | Result r = await result; 57 | return await r.Combine(action); 58 | } 59 | catch (Exception e) 60 | { 61 | ExceptionDispatchInfo.Capture(e).Throw(); 62 | throw; 63 | } 64 | } 65 | 66 | /// 67 | /// Allows to combine the result of two methods. the input of the combined method is the result of the first method. 68 | /// 69 | /// A result chain that contains a tuple with both results 70 | public static async Task> Combine(this Task> result, Func> action) 71 | { 72 | try 73 | { 74 | Result r = await result; 75 | return r.Combine(action); 76 | } 77 | catch (Exception e) 78 | { 79 | ExceptionDispatchInfo.Capture(e).Throw(); 80 | throw; 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/ROP/Result_Fallback.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace ROP 6 | { 7 | /// 8 | /// Provides extension methods for handling fallback logic in result chains. 9 | /// 10 | public static class Result_Fallback 11 | { 12 | /// 13 | /// The method gets executed IF the chain is in Error state, 14 | /// the previous information will be lost 15 | /// 16 | /// The original result if successful; otherwise, the result of the fallback method. 17 | public static Result Fallback(this Result r, Func> method) 18 | { 19 | try 20 | { 21 | return r.Success 22 | ? r.Value 23 | : method(r.Value); 24 | } 25 | catch (Exception e) 26 | { 27 | ExceptionDispatchInfo.Capture(e).Throw(); 28 | throw; 29 | } 30 | } 31 | 32 | /// 33 | /// The method gets executed IF the chain is in Error state, 34 | /// the previous information will be lost 35 | /// 36 | /// The original result if successful; otherwise, the result of the fallback method. 37 | public static async Task> Fallback(this Result r, Func>> method) 38 | { 39 | try 40 | { 41 | return r.Success 42 | ? r.Value 43 | : await method(r.Value); 44 | } 45 | catch (Exception e) 46 | { 47 | ExceptionDispatchInfo.Capture(e).Throw(); 48 | throw; 49 | } 50 | } 51 | 52 | /// 53 | /// The method gets executed IF the chain is in Error state, 54 | /// the previous information will be lost 55 | /// 56 | /// The original result if successful; otherwise, the result of the fallback method. 57 | public static async Task> Fallback(this Task> result, Func>> method) 58 | { 59 | try 60 | { 61 | var r = await result; 62 | return await r.Fallback(method); 63 | } 64 | catch (Exception e) 65 | { 66 | ExceptionDispatchInfo.Capture(e).Throw(); 67 | throw; 68 | } 69 | } 70 | 71 | /// 72 | /// The method gets executed IF the chain is in Error state, 73 | /// the previous information will be lost 74 | /// 75 | /// The original result if successful; otherwise, the result of the fallback method. 76 | public static async Task> Fallback(this Task> result, Func> method) 77 | { 78 | try 79 | { 80 | var r = await result; 81 | return r.Fallback(method); 82 | } 83 | catch (Exception e) 84 | { 85 | ExceptionDispatchInfo.Capture(e).Throw(); 86 | throw; 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/ROP/Result_Ignore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace ROP 6 | { 7 | /// 8 | /// Provides extension methods for ignoring the result of a chain. 9 | /// 10 | public static class Result_Ignore 11 | { 12 | /// 13 | /// Similar to fire and forget, the method gets executed, but the response is ignored 14 | /// 15 | /// A Unit Result indicating success or failure without a value. 16 | public static Result Ignore(this Result r) 17 | { 18 | try 19 | { 20 | return r.Bind(x => Result.Success()); 21 | } 22 | catch (Exception e) 23 | { 24 | ExceptionDispatchInfo.Capture(e).Throw(); 25 | throw; 26 | } 27 | } 28 | 29 | /// 30 | /// Similar to fire and forget, the method gets executed, but the response is ignored 31 | /// 32 | /// A Unit Result indicating success or failure without a value. 33 | public static async Task> Ignore(this Task> r) 34 | { 35 | try 36 | { 37 | return (await r).Ignore(); 38 | } 39 | catch (Exception e) 40 | { 41 | ExceptionDispatchInfo.Capture(e).Throw(); 42 | throw; 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/ROP/Result_Map.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace ROP 6 | { 7 | /// 8 | /// Provides extension methods for mapping results from one type to another. 9 | /// 10 | public static class Result_Map 11 | { 12 | /// 13 | /// Allows to get map from a result T to U, the mapper method do not need to return a result T 14 | /// 15 | /// A result of type U. 16 | public static Result Map(this Result r, Func mapper) 17 | { 18 | try 19 | { 20 | return r.Bind(x => mapper(x).Success()); 21 | } 22 | catch (Exception e) 23 | { 24 | ExceptionDispatchInfo.Capture(e).Throw(); 25 | throw; 26 | } 27 | } 28 | 29 | /// 30 | /// Allows to get map from a result T to U, the mapper method do not need to return a result T 31 | /// 32 | /// A result of type U. 33 | public static async Task> Map(this Result r, Func> mapper) 34 | { 35 | try 36 | { 37 | return await r.Bind(async x => (await mapper(x)).Success()); 38 | } 39 | catch (Exception e) 40 | { 41 | ExceptionDispatchInfo.Capture(e).Throw(); 42 | throw; 43 | } 44 | } 45 | 46 | /// 47 | /// Allows to get map from a result T to U, the mapper method do not need to return a result T 48 | /// 49 | /// A result of type U. 50 | public static async Task> Map(this Task> result, Func> mapper) 51 | { 52 | try 53 | { 54 | var r = await result; 55 | return await r.Map(mapper); 56 | } 57 | catch (Exception e) 58 | { 59 | ExceptionDispatchInfo.Capture(e).Throw(); 60 | throw; 61 | } 62 | } 63 | 64 | /// 65 | /// Allows to get map from a result T to U, the mapper method do not need to return a result T 66 | /// 67 | /// A result of type U. 68 | public static async Task> Map(this Task> result, Func mapper) 69 | { 70 | try 71 | { 72 | var r = await result; 73 | return r.Map(mapper); 74 | } 75 | catch (Exception e) 76 | { 77 | ExceptionDispatchInfo.Capture(e).Throw(); 78 | throw; 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/ROP/Result_SuccessHTTPStatusCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Runtime.ExceptionServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace ROP 7 | { 8 | /// 9 | /// Provides extension methods for handling success status codes in result chains. 10 | /// 11 | public static class Result_SuccessHTTPStatusCode 12 | { 13 | /// 14 | /// It allows to specify a success status code, handy if for example you are returning the chain on an API. 15 | /// 16 | public static Result UseSuccessHttpStatusCode(this Result r, HttpStatusCode httpStatusCode) 17 | { 18 | try 19 | { 20 | return r.Bind(x => x.Success(httpStatusCode)); 21 | } 22 | catch (Exception e) 23 | { 24 | ExceptionDispatchInfo.Capture(e).Throw(); 25 | throw; 26 | } 27 | } 28 | 29 | /// 30 | /// It allows to specify a success status code, handy if for example you are returning the chain on an API. 31 | /// 32 | public static async Task> UseSuccessHttpStatusCode(this Task> result, HttpStatusCode httpStatusCode) 33 | { 34 | try 35 | { 36 | var r = await result; 37 | return r.UseSuccessHttpStatusCode(httpStatusCode); 38 | } 39 | catch (Exception e) 40 | { 41 | ExceptionDispatchInfo.Capture(e).Throw(); 42 | throw; 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/ROP/Result_T.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Net; 4 | 5 | namespace ROP 6 | { 7 | /// 8 | /// Represents the result of an operation, containing either a value of type T or a collection of errors. 9 | /// 10 | /// The type of the value contained in the result. 11 | public struct Result 12 | { 13 | /// 14 | /// Gets the value of the result if the operation was successful. 15 | /// 16 | public readonly T Value; 17 | 18 | /// 19 | /// Implicitly converts a value of type T to a successful result with an HTTP status code of OK. 20 | /// 21 | /// The value to convert. 22 | public static implicit operator Result(T value) => new Result(value, HttpStatusCode.OK); 23 | 24 | /// 25 | /// Implicitly converts a collection of errors to a failure result with an HTTP status code of BadRequest. 26 | /// 27 | /// The collection of errors to convert. 28 | public static implicit operator Result(ImmutableArray errors) => new Result(errors, HttpStatusCode.BadRequest); 29 | 30 | /// 31 | /// Gets the collection of errors. 32 | /// 33 | public readonly ImmutableArray Errors; 34 | 35 | /// 36 | /// Gets the HTTP status code associated with the result. 37 | /// 38 | public readonly HttpStatusCode HttpStatusCode; 39 | 40 | /// 41 | /// Gets a value indicating whether the operation was successful. 42 | /// 43 | public bool Success => Errors.Length == 0; 44 | 45 | /// 46 | /// Initializes a new instance of the struct with a successful value and status code. 47 | /// 48 | /// The value of the result. 49 | /// The HTTP status code associated with the result. 50 | public Result(T value, HttpStatusCode statusCode) 51 | { 52 | Value = value; 53 | Errors = ImmutableArray.Empty; 54 | HttpStatusCode = statusCode; 55 | } 56 | 57 | /// 58 | /// Initializes a new instance of the struct with a collection of errors and status code. 59 | /// 60 | /// The collection of errors. 61 | /// The HTTP status code associated with the result. 62 | /// Thrown when the errors collection is empty. 63 | public Result(ImmutableArray errors, HttpStatusCode statusCode) 64 | { 65 | if (errors.Length == 0) 66 | { 67 | throw new InvalidOperationException("You should specify at least one error"); 68 | } 69 | 70 | HttpStatusCode = statusCode; 71 | Value = default(T); 72 | Errors = errors; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ROP/Result_Then.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace ROP 6 | { 7 | /// 8 | /// Provides extension methods for executing additional methods in a result chain while ignoring their results. 9 | /// 10 | public static class Result_Then 11 | { 12 | /// 13 | /// Allows to execute a method on the chain, but the result is the output of the caller 14 | /// (its result gets ignored) Example: 15 | /// method 1-> returns int 16 | /// thenMethod returns string 17 | /// value on the chain -> int 18 | /// 19 | /// The original result if successful; otherwise, a failure result. 20 | public static Result Then(this Result r, Func> method) 21 | { 22 | try 23 | { 24 | return r.Bind(method) 25 | .Map(_ => r.Value); 26 | } 27 | catch (Exception e) 28 | { 29 | ExceptionDispatchInfo.Capture(e).Throw(); 30 | throw; 31 | } 32 | } 33 | 34 | /// 35 | /// Allows to execute a method on the chain, but the result is the output of the caller 36 | /// (its result gets ignored) Example: 37 | /// method 1-> returns int 38 | /// thenMethod returns string 39 | /// value on the chain -> int 40 | /// 41 | /// The original result if successful; otherwise, a failure result. 42 | public static async Task> Then(this Result r, Func>> method) 43 | { 44 | try 45 | { 46 | return await r.Bind(method) 47 | .Map(_ => r.Value); 48 | } 49 | catch (Exception e) 50 | { 51 | ExceptionDispatchInfo.Capture(e).Throw(); 52 | throw; 53 | } 54 | } 55 | 56 | /// 57 | /// Allows to execute a method on the chain, but the result is the output of the caller 58 | /// (its result gets ignored) Example: 59 | /// method 1-> returns int 60 | /// thenMethod returns string 61 | /// value on the chain -> int 62 | /// 63 | /// The original result if successful; otherwise, a failure result. 64 | public static async Task> Then(this Task> result, Func>> method) 65 | { 66 | try 67 | { 68 | var r = await result; 69 | return await r.Then(method); 70 | } 71 | catch (Exception e) 72 | { 73 | ExceptionDispatchInfo.Capture(e).Throw(); 74 | throw; 75 | } 76 | } 77 | 78 | /// 79 | /// Allows to execute a method on the chain, but the result is the output of the caller 80 | /// (its result gets ignored) Example: 81 | /// method 1-> returns int 82 | /// thenMethod returns string 83 | /// value on the chain -> int 84 | /// 85 | /// The original result if successful; otherwise, a failure result. 86 | public static async Task> Then(this Task> result, Func> method) 87 | { 88 | try 89 | { 90 | var r = await result; 91 | return r.Then(method); 92 | } 93 | catch (Exception e) 94 | { 95 | ExceptionDispatchInfo.Capture(e).Throw(); 96 | throw; 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/ROP/Result_Throw.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Threading.Tasks; 3 | 4 | namespace ROP 5 | { 6 | /// 7 | /// Provides extension methods for throwing exceptions based on result states. 8 | /// 9 | public static class Result_Throw 10 | { 11 | /// 12 | /// Converts Result T into T, but if there is any exception, it throws it 13 | /// 14 | /// result structure 15 | /// Type 16 | /// the object on T 17 | public static T Throw(this Result r) 18 | { 19 | if (!r.Success) 20 | { 21 | Throw(r.Errors); 22 | } 23 | 24 | return r.Value; 25 | } 26 | 27 | /// 28 | /// Converts the errors in the Array on the content of the exception 29 | /// 30 | public static void Throw(this ImmutableArray errors) 31 | { 32 | if (errors.Length > 0) 33 | { 34 | throw new ErrorResultException(errors); 35 | } 36 | } 37 | 38 | /// 39 | /// Converts Result T into T, but if there is any exception, it throws it (async) 40 | /// 41 | /// result structure 42 | /// Type 43 | /// the object on T 44 | public static async Task ThrowAsync(this Task> result) 45 | { 46 | return (await result).Throw(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ROP/Result_Traverse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Net; 5 | using System.Runtime.ExceptionServices; 6 | using System.Threading.Tasks; 7 | 8 | namespace ROP 9 | { 10 | /// 11 | /// Provides extension methods for traversing collections of results. 12 | /// 13 | public static class Result_Traverse 14 | { 15 | 16 | /// 17 | /// Converts a IEnumerable result T into a result list T 18 | /// 19 | /// A Result containing a list of T if all results are successful; otherwise, a failure result. 20 | public static Result> Traverse(this IEnumerable> results) 21 | { 22 | try 23 | { 24 | List errors = new List(); 25 | List output = new List(); 26 | HttpStatusCode firstStatusCode = HttpStatusCode.BadRequest; 27 | 28 | foreach (var r in results) 29 | { 30 | if (r.Success) 31 | { 32 | output.Add(r.Value); 33 | } 34 | else 35 | { 36 | if (errors.Count == 0) firstStatusCode = r.HttpStatusCode; 37 | 38 | errors.AddRange(r.Errors); 39 | } 40 | } 41 | 42 | return errors.Count > 0 43 | ? Result.Failure>(errors.ToImmutableArray(), firstStatusCode) 44 | : Result.Success(output); 45 | } 46 | catch (Exception e) 47 | { 48 | ExceptionDispatchInfo.Capture(e).Throw(); 49 | throw; 50 | } 51 | } 52 | 53 | /// 54 | /// Converts a IEnumerable result T into a Result list T 55 | /// 56 | /// A Result containing a list of T if all results are successful; otherwise, a failure result. 57 | public static async Task>> Traverse(this IEnumerable>> results) 58 | { 59 | try 60 | { 61 | return (await Task.WhenAll(results)).Traverse(); 62 | } 63 | catch (Exception e) 64 | { 65 | ExceptionDispatchInfo.Capture(e).Throw(); 66 | throw; 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/ROP/Unit.cs: -------------------------------------------------------------------------------- 1 | namespace ROP 2 | { 3 | /// 4 | /// Represents a unit type, which represents the absence of a specific value. 5 | /// 6 | public sealed class Unit 7 | { 8 | /// 9 | /// The single instance of the Unit type. 10 | /// 11 | public static readonly Unit Value = new Unit(); 12 | private Unit() { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/ROP.Ejemplo.CasoDeUso/AddUser/AddUserPOOService.cs: -------------------------------------------------------------------------------- 1 | using ROP.Ejemplo.CasoDeUso.DTO; 2 | 3 | namespace ROP.Ejemplo.CasoDeUso.AddUser 4 | { 5 | 6 | public interface IAddUserPOOServiceDependencies 7 | { 8 | bool AddUser(UserAccount userAccount); 9 | bool EnviarCorreo(UserAccount userAccount); 10 | } 11 | 12 | 13 | /// 14 | /// Añadir el usuario utilizando una estructura de programación orientada a objetos. 15 | /// 16 | public class AddUserPOOService 17 | { 18 | private readonly IAddUserPOOServiceDependencies _dependencies; 19 | public AddUserPOOService(IAddUserPOOServiceDependencies dependencies) 20 | { 21 | _dependencies = dependencies; 22 | } 23 | 24 | public string AddUser(UserAccount userAccount) 25 | { 26 | var validacionUsuario = ValidateUser(userAccount); 27 | if (!string.IsNullOrWhiteSpace(validacionUsuario)) 28 | { 29 | return validacionUsuario; 30 | 31 | } 32 | 33 | var addUserDB = AddUserToDatabase(userAccount); 34 | if (!string.IsNullOrWhiteSpace(addUserDB)) 35 | { 36 | return addUserDB; 37 | 38 | } 39 | var sendEmail = SendEmail(userAccount); 40 | if (!string.IsNullOrWhiteSpace(sendEmail)) 41 | { 42 | return sendEmail; 43 | } 44 | 45 | return "Usuario añadido correctamente"; 46 | } 47 | 48 | private string ValidateUser(UserAccount userAccount) 49 | { 50 | if (string.IsNullOrWhiteSpace(userAccount.FirstName)) 51 | return "El nombre propio no puede estar vacio"; 52 | if (string.IsNullOrWhiteSpace(userAccount.LastName)) 53 | return "El apellido propio no puede estar vacio"; 54 | if (string.IsNullOrWhiteSpace(userAccount.UserName)) 55 | return "El nombre de usuario no debe estar vacio"; 56 | 57 | return ""; 58 | } 59 | 60 | private string AddUserToDatabase(UserAccount userAccount) 61 | { 62 | if (!_dependencies.AddUser(userAccount)) 63 | { 64 | return "Error añadiendo el usuario en la base de datos"; 65 | } 66 | 67 | return ""; 68 | } 69 | private string SendEmail(UserAccount userAccount) 70 | { 71 | if (!_dependencies.EnviarCorreo(userAccount)) 72 | { 73 | return "Error enviando el correo al email del usuario"; 74 | } 75 | 76 | return ""; 77 | } 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/ROP.Ejemplo.CasoDeUso/AddUser/AdduserROPService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ROP.Ejemplo.CasoDeUso.DTO; 3 | using System.Linq; 4 | using System.Collections.Immutable; 5 | 6 | namespace ROP.Ejemplo.CasoDeUso.AddUser 7 | { 8 | public interface IAdduserROPServiceDependencies 9 | { 10 | Result AddUser(UserAccount userAccount); 11 | Result EnviarCorreo(string email); 12 | } 13 | 14 | public class AdduserROPService 15 | { 16 | private readonly IAdduserROPServiceDependencies _dependencies; 17 | public AdduserROPService(IAdduserROPServiceDependencies dependencies) 18 | { 19 | _dependencies = dependencies; 20 | } 21 | 22 | public Result AddUser(UserAccount userAccount) 23 | { 24 | return ValidateUser(userAccount) 25 | .Bind(AddUserToDatabase) 26 | .Bind(SendEmail) 27 | .Map(_ => userAccount); 28 | } 29 | 30 | private Result ValidateUser(UserAccount userAccount) 31 | { 32 | List errores = new List(); 33 | 34 | if (string.IsNullOrWhiteSpace(userAccount.FirstName)) 35 | errores.Add(Error.Create("El nombre propio no puede estar vacio")); 36 | if (string.IsNullOrWhiteSpace(userAccount.LastName)) 37 | errores.Add(Error.Create("El apellido propio no puede estar vacio")); 38 | if (string.IsNullOrWhiteSpace(userAccount.UserName)) 39 | errores.Add(Error.Create("El nombre de usuario no debe estar vacio")); 40 | 41 | return errores.Any() 42 | ? Result.Failure(errores.ToImmutableArray()) 43 | : userAccount; 44 | } 45 | 46 | private Result AddUserToDatabase(UserAccount userAccount) 47 | { 48 | return _dependencies.AddUser(userAccount) 49 | .Map(_ => userAccount.Email); 50 | } 51 | 52 | private Result SendEmail(string email) 53 | { 54 | return _dependencies.EnviarCorreo(email); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/ROP.Ejemplo.CasoDeUso/DTO/UserAccount.cs: -------------------------------------------------------------------------------- 1 | namespace ROP.Ejemplo.CasoDeUso.DTO 2 | { 3 | public class UserAccount 4 | { 5 | public readonly string UserName; 6 | public readonly string FirstName; 7 | public readonly string LastName; 8 | public readonly string Email; 9 | 10 | public UserAccount(string userName, string firstName, string lastName, string email) 11 | { 12 | UserName = userName; 13 | FirstName = firstName; 14 | LastName = lastName; 15 | Email = email; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/ROP.Ejemplo.CasoDeUso/ROP.Ejemplo.CasoDeUso.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/ApiExtensions/Translations/ErrorTranslations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ROP.UnitTest.ApiExtensions.Translations 4 | { 5 | public class ErrorTranslations 6 | { 7 | public static readonly Guid ErrorExample = Guid.Parse("0b002212-4e6b-4561-96f7-8a06dbc65ac3"); 8 | public static readonly Guid ErrorExampleWithVariables = Guid.Parse("a5b62027-9705-40b2-a646-fe91d0353145"); 9 | } 10 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/ApiExtensions/Translations/ErrorTranslations.en.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | text/microsoft-resx 11 | 12 | 13 | 1.3 14 | 15 | 16 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 17 | 18 | 19 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 20 | 21 | 22 | This is the message Translated 23 | 24 | 25 | message translated with variable of value {0} and a second one {1} 26 | 27 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/ApiExtensions/Translations/TestErrorDtoSerializerSerialization.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using Microsoft.AspNetCore.Http; 6 | using Moq; 7 | using ROP.APIExtensions; 8 | using ROP.ApiExtensions.Translations.Serializers; 9 | using Xunit; 10 | 11 | namespace ROP.UnitTest.ApiExtensions.Translations 12 | { 13 | public class TestErrorDtoSerializerSerialization 14 | { 15 | [Fact] 16 | public void When_message_is_empty_then_translate() 17 | { 18 | JsonSerializerOptions serializeOptions = GetSerializerOptions(); 19 | 20 | ResultDto obj = new ResultDto() 21 | { 22 | Value = null, 23 | Errors = new List() 24 | { 25 | new ErrorDto() 26 | { 27 | ErrorCode = ErrorTranslations.ErrorExample 28 | } 29 | }.ToImmutableArray() 30 | }; 31 | 32 | string json = JsonSerializer.Serialize(obj, serializeOptions); 33 | var resultDto = JsonSerializer.Deserialize>(json); 34 | Assert.Equal("This is the message Translated", resultDto.Errors.First().Message); 35 | } 36 | 37 | [Fact] 38 | public void When_message_is_populated_translation_getsIgnored() 39 | { 40 | 41 | JsonSerializerOptions serializeOptions = GetSerializerOptions(); 42 | 43 | ResultDto obj = new ResultDto() 44 | { 45 | Value = null, 46 | Errors = new List() 47 | { 48 | new ErrorDto() 49 | { 50 | Message = "example message", 51 | ErrorCode = ErrorTranslations.ErrorExample 52 | } 53 | }.ToImmutableArray() 54 | }; 55 | 56 | string json = JsonSerializer.Serialize(obj, serializeOptions); 57 | var resultDto = JsonSerializer.Deserialize>(json); 58 | Assert.Equal(obj.Errors.First().Message, resultDto.Errors.First().Message); 59 | } 60 | 61 | [Fact] 62 | public void When_translation_contains_variables_message_gets_fromated() 63 | { 64 | JsonSerializerOptions serializeOptions = GetSerializerOptions(); 65 | 66 | ResultDto obj = new ResultDto() 67 | { 68 | Value = null, 69 | Errors = new List() 70 | { 71 | new ErrorDto() 72 | { 73 | ErrorCode = ErrorTranslations.ErrorExampleWithVariables, 74 | TranslationVariables = new []{"1", "2"} 75 | } 76 | }.ToImmutableArray() 77 | }; 78 | 79 | string json = JsonSerializer.Serialize(obj, serializeOptions); 80 | var resultDto = JsonSerializer.Deserialize>(json); 81 | Assert.Equal("message translated with variable of value 1 and a second one 2", resultDto.Errors.First().Message); 82 | } 83 | 84 | private JsonSerializerOptions GetSerializerOptions() 85 | { 86 | Mock mockHeader = new Mock(); 87 | mockHeader.Setup(a => a["Accept-Language"]).Returns("en;q=0.4"); 88 | Mock httpContextAccessorMock = new Mock(); 89 | httpContextAccessorMock.Setup(a => a.HttpContext.Request.Headers).Returns(mockHeader.Object); 90 | 91 | JsonSerializerOptions serializeOptions = new JsonSerializerOptions(); 92 | serializeOptions.Converters.Add(new ErrorDtoSerializer(httpContextAccessorMock.Object)); 93 | return serializeOptions; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/BaseResultTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace ROP.UnitTest 5 | { 6 | public class BaseResultTest 7 | { 8 | protected Result IntToString(int i) 9 | => i.ToString(); 10 | protected Task> IntToStringAsync(int i) 11 | => IntToString(i).Async(); 12 | protected Result IntToStringFailure(int i) 13 | => Result.Failure("There is an error"); 14 | protected Task> IntToStringAsyncFailure(int i) 15 | => IntToStringFailure(i).Async(); 16 | 17 | protected Result StringIntoInt(string s) 18 | => Convert.ToInt32(s); 19 | protected Task> StringIntoIntAsync(string s) 20 | => StringIntoInt(s).Async(); 21 | protected Result StringIntoIntFailure(string s) 22 | => Result.NotFound("There is an error"); 23 | protected Task> StringIntoIntAsyncFailure(string s) 24 | => StringIntoIntFailure(s).Async(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/ROP.UnitTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ResXFileCodeGenerator 27 | TraduccionErrores.en.Designer.cs 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/ResultFailure/TestBaseResultFailure.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using Xunit; 4 | 5 | namespace ROP.UnitTest.ResultFailure 6 | { 7 | public abstract class TestBaseResultFailure 8 | { 9 | [Fact] 10 | public void TestResultOnStringError_ThenStatusCode() 11 | { 12 | Result result = GetResultWithString(); 13 | Assert.Equal(GetExpectedHttpStatusCode(), result.HttpStatusCode); 14 | } 15 | 16 | [Fact] 17 | public void TestResultOnGuid_ThenMessageIsEmpty() 18 | { 19 | Result result = GetResultWithGuid(); 20 | Assert.Empty(result.Errors.First().Message); 21 | Assert.Equal(GetExpectedHttpStatusCode(), result.HttpStatusCode); 22 | } 23 | 24 | [Fact] 25 | public void TestResultOnError_ThenStatusCode() 26 | { 27 | Result result = GetResultWithError(); 28 | Assert.Equal(GetExpectedHttpStatusCode(), result.HttpStatusCode); 29 | } 30 | 31 | [Fact] 32 | public void TestResultOnArray_ThenStatusCode() 33 | { 34 | Result result = GetResultWithArray(); 35 | Assert.Equal(GetExpectedHttpStatusCode(), result.HttpStatusCode); 36 | } 37 | 38 | [Fact] 39 | public void TestResultOnIEnumerable_ThenStatusCode() 40 | { 41 | Result result = GetResultWithIEnumerable(); 42 | Assert.Equal(GetExpectedHttpStatusCode(), result.HttpStatusCode); 43 | } 44 | 45 | [Fact] 46 | public void TestResultTypedOnStringError_ThenStatusCode() 47 | { 48 | Result result = GetTypedResultWithString(); 49 | Assert.Equal(GetExpectedHttpStatusCode(), result.HttpStatusCode); 50 | } 51 | 52 | [Fact] 53 | public void TestResultTypedOnError_ThenStatusCode() 54 | { 55 | Result result = GetTypedResultWithError(); 56 | Assert.Equal(GetExpectedHttpStatusCode(), result.HttpStatusCode); 57 | } 58 | 59 | [Fact] 60 | public void TestResultTypedOnArray_ThenStatusCode() 61 | { 62 | Result result = GetTypedResultWithArray(); 63 | Assert.Equal(GetExpectedHttpStatusCode(), result.HttpStatusCode); 64 | } 65 | 66 | protected abstract Result GetResultWithString(); 67 | protected abstract Result GetResultWithGuid(); 68 | protected abstract Result GetResultWithError(); 69 | protected abstract Result GetResultWithArray(); 70 | protected abstract Result GetResultWithIEnumerable(); 71 | protected abstract Result GetTypedResultWithString(); 72 | protected abstract Result GetTypedResultWithError(); 73 | protected abstract Result GetTypedResultWithArray(); 74 | protected abstract HttpStatusCode GetExpectedHttpStatusCode(); 75 | } 76 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/ResultFailure/TestResultBadRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Net; 5 | 6 | namespace ROP.UnitTest.ResultFailure 7 | { 8 | public class TestResultBadRequest: TestBaseResultFailure 9 | { 10 | protected override Result GetResultWithString() => Result.BadRequest("Error"); 11 | protected override Result GetResultWithError() => Result.BadRequest(Error.Create("error")); 12 | protected override Result GetResultWithGuid() => Result.BadRequest(Guid.NewGuid()); 13 | protected override Result GetResultWithArray() => Result.BadRequest(ImmutableArray.Create(Error.Create("Error"))); 14 | protected override Result GetResultWithIEnumerable() => Result.BadRequest(new List() {Error.Create("example")}); 15 | protected override Result GetTypedResultWithString() => Result.BadRequest("Error"); 16 | protected override Result GetTypedResultWithError() => Result.BadRequest(Error.Create("error")); 17 | protected override Result GetTypedResultWithArray() => Result.BadRequest(ImmutableArray.Create(Error.Create("Error"))); 18 | protected override HttpStatusCode GetExpectedHttpStatusCode() => HttpStatusCode.BadRequest; 19 | } 20 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/ResultFailure/TestResultConflict.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Net; 5 | namespace ROP.UnitTest.ResultFailure 6 | { 7 | public class TestResultConflict: TestBaseResultFailure 8 | { 9 | protected override Result GetResultWithString() => Result.Conflict("Error"); 10 | protected override Result GetResultWithGuid() => Result.Conflict(Guid.NewGuid()); 11 | protected override Result GetResultWithError() => Result.Conflict(Error.Create("error")); 12 | protected override Result GetResultWithArray() => Result.Conflict(ImmutableArray.Create(Error.Create("Error"))); 13 | protected override Result GetResultWithIEnumerable() => Result.Conflict(new List() {Error.Create("example")}); 14 | protected override Result GetTypedResultWithString() => Result.Conflict("Error"); 15 | protected override Result GetTypedResultWithError() => Result.Conflict(Error.Create("error")); 16 | protected override Result GetTypedResultWithArray() => Result.Conflict(ImmutableArray.Create(Error.Create("Error"))); 17 | protected override HttpStatusCode GetExpectedHttpStatusCode() => HttpStatusCode.Conflict; 18 | } 19 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/ResultFailure/TestResultFailure.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Net; 5 | 6 | namespace ROP.UnitTest.ResultFailure 7 | { 8 | public class TestResultFailure : TestBaseResultFailure 9 | { 10 | protected override Result GetResultWithString() => Result.Failure("Error"); 11 | protected override Result GetResultWithGuid() => Result.Failure(Guid.NewGuid()); 12 | protected override Result GetResultWithError() => Result.Failure(Error.Create("error")); 13 | protected override Result GetResultWithArray() => Result.Failure(ImmutableArray.Create(Error.Create("Error"))); 14 | protected override Result GetResultWithIEnumerable() => Result.Failure(new List() {Error.Create("example")}); 15 | protected override Result GetTypedResultWithString() => Result.Failure("Error"); 16 | protected override Result GetTypedResultWithError() => Result.Failure(Error.Create("error")); 17 | protected override Result GetTypedResultWithArray() => Result.Failure(ImmutableArray.Create(Error.Create("Error"))); 18 | protected override HttpStatusCode GetExpectedHttpStatusCode() => HttpStatusCode.BadRequest; 19 | } 20 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/ResultFailure/TestResultNotFound.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Net; 5 | 6 | namespace ROP.UnitTest.ResultFailure 7 | { 8 | public class TestResultNotFound : TestBaseResultFailure 9 | { 10 | protected override Result GetResultWithString() => Result.NotFound("Error"); 11 | protected override Result GetResultWithGuid() => Result.NotFound(Guid.NewGuid()); 12 | protected override Result GetResultWithError() => Result.NotFound(Error.Create("error")); 13 | protected override Result GetResultWithArray() => Result.NotFound(ImmutableArray.Create(Error.Create("Error"))); 14 | protected override Result GetResultWithIEnumerable() => Result.NotFound(new List() {Error.Create("example")}); 15 | protected override Result GetTypedResultWithString() => Result.NotFound("Error"); 16 | protected override Result GetTypedResultWithError() => Result.NotFound(Error.Create("error")); 17 | protected override Result GetTypedResultWithArray() => Result.NotFound(ImmutableArray.Create(Error.Create("Error"))); 18 | protected override HttpStatusCode GetExpectedHttpStatusCode() => HttpStatusCode.NotFound; 19 | } 20 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/Serializer/TestResultDtoOnSerialization.cs: -------------------------------------------------------------------------------- 1 | using ROP.APIExtensions; 2 | using System.Collections.Immutable; 3 | using System.Text.Json; 4 | using Xunit; 5 | 6 | namespace ROP.UnitTest.Serializer 7 | { 8 | public class TestResultDtoOnSerialization 9 | { 10 | [Fact] 11 | public void Test() 12 | { 13 | ResultDto obj = new ResultDto() 14 | { 15 | Value = new PlaceHolder(1), 16 | Errors = ImmutableArray.Empty 17 | }; 18 | 19 | string json = JsonSerializer.Serialize(obj); 20 | 21 | Assert.NotEmpty(json); 22 | 23 | var resultDto = JsonSerializer.Deserialize>(json); 24 | Assert.Equal(obj.Value.Id, resultDto.Value.Id); 25 | 26 | } 27 | 28 | 29 | public class PlaceHolder 30 | { 31 | public int Id { get; private set; } 32 | 33 | public PlaceHolder(int id) 34 | { 35 | Id = id; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestActionResultExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | using ROP.APIExtensions; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System.Net; 9 | 10 | namespace ROP.UnitTest 11 | { 12 | public class TestActionResultExtensions 13 | { 14 | [Fact] 15 | public async Task TestBindAllCorrect() 16 | { 17 | int originalValue = 1234; 18 | IActionResult apiResult = await originalValue.Success().Async().ToActionResult(); 19 | 20 | ObjectResult result = apiResult as ObjectResult; 21 | ResultDto resultVaue = result.Value as ResultDto; 22 | 23 | Assert.Equal(originalValue, resultVaue.Value); 24 | Assert.Equal((int)HttpStatusCode.OK, result.StatusCode); 25 | } 26 | 27 | [Fact] 28 | public async Task WhenSuccess_andToResultOrProblemDetails_ReturnObject() 29 | { 30 | int originalValue = 1234; 31 | IActionResult apiResult = await originalValue.Success().Async().ToValueOrProblemDetails(); 32 | 33 | ObjectResult result = apiResult as ObjectResult; 34 | int? resultVaue = result.Value as int?; 35 | Assert.Equal(originalValue, resultVaue); 36 | Assert.Equal((int)HttpStatusCode.OK, result.StatusCode); 37 | } 38 | 39 | [Fact] 40 | public async Task WhenErrorWithOnlyMessage_andToResultOrProblemDetails_ReturnProblemDetailsWithNoErrorCode() 41 | { 42 | string originalErrorValue = "error"; 43 | IActionResult apiResult = 44 | await Result.BadRequest(originalErrorValue).Async().ToValueOrProblemDetails(); 45 | 46 | ObjectResult result = apiResult as ObjectResult; 47 | ProblemDetails resultVaue = result.Value as ProblemDetails; 48 | Assert.Equal((int)HttpStatusCode.BadRequest, result.StatusCode); 49 | Assert.Equal("Error(s) found", resultVaue.Title); 50 | Assert.Equal("One or more errors occurred", resultVaue.Detail); 51 | Assert.Single(resultVaue.Extensions); 52 | var extension = resultVaue.Extensions.First(); 53 | Assert.Equal("ValidationErrors", extension.Key); 54 | var errorDtos = extension.Value as List; 55 | Assert.Single(errorDtos); 56 | Assert.Equal(originalErrorValue, errorDtos.First().Message); 57 | Assert.Null(errorDtos.First().ErrorCode); 58 | } 59 | 60 | [Fact] 61 | public async Task WhenErrorWithError_andToResultOrProblemDetails_ReturnProblemDetails() 62 | { 63 | Error originalErrorValue = Error.Create("ErrorMessage", Guid.NewGuid()); 64 | IActionResult apiResult = 65 | await Result.BadRequest(originalErrorValue).Async().ToValueOrProblemDetails(); 66 | 67 | ObjectResult result = apiResult as ObjectResult; 68 | ProblemDetails resultVaue = result.Value as ProblemDetails; 69 | Assert.Equal((int)HttpStatusCode.BadRequest, result.StatusCode); 70 | Assert.Equal("Error(s) found", resultVaue.Title); 71 | Assert.Equal("One or more errors occurred", resultVaue.Detail); 72 | Assert.Single(resultVaue.Extensions); 73 | var extension = resultVaue.Extensions.First(); 74 | Assert.Equal("ValidationErrors", extension.Key); 75 | var errorDtos = extension.Value as List; 76 | Assert.Single(errorDtos); 77 | Assert.Equal(originalErrorValue.ErrorCode, errorDtos.First().ErrorCode); 78 | Assert.Equal(originalErrorValue.Message, errorDtos.First().Message); 79 | Assert.Equal(originalErrorValue.TranslationVariables, errorDtos.First().TranslationVariables); 80 | 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace ROP.UnitTest; 5 | 6 | public class TestAsync 7 | { 8 | [Fact] 9 | public async Task WhenMethodReturningUnitFails_thenAsync_ReturnsError() 10 | { 11 | var result = await FailedMethod() 12 | .Async(); 13 | 14 | Assert.False(result.Success); 15 | } 16 | 17 | private Result FailedMethod() 18 | { 19 | return Result.Failure("example"); 20 | } 21 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestBind.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace ROP.UnitTest 7 | { 8 | public class TestBind : BaseResultTest 9 | { 10 | 11 | [Fact] 12 | public void TestBindAllCorrect() 13 | { 14 | int originalValue = 1; 15 | 16 | Result result = IntToString(originalValue) 17 | .Bind(StringIntoInt); 18 | 19 | Assert.True(result.Success); 20 | Assert.Equal(originalValue, result.Value); 21 | } 22 | 23 | [Fact] 24 | public async Task TestBindAsyncAllCorrect() 25 | { 26 | int originalValue = 1; 27 | 28 | Result result = await IntToStringAsync(originalValue) 29 | .Bind(StringIntoIntAsync); 30 | 31 | Assert.True(result.Success); 32 | Assert.Equal(originalValue, result.Value); 33 | } 34 | 35 | [Fact] 36 | public void TestBindWithFailure() 37 | { 38 | int originalValue = 1; 39 | 40 | Result result = IntToString(originalValue) 41 | .Bind(StringIntoIntFailure); 42 | 43 | Assert.False(result.Success); 44 | Assert.Equal(default(int), result.Value); 45 | Assert.Single(result.Errors); 46 | Assert.Contains("error", result.Errors.First().Message); 47 | Assert.Equal(HttpStatusCode.NotFound, result.HttpStatusCode); 48 | } 49 | 50 | [Fact] 51 | public async Task TestBindWithFailureAsync() 52 | { 53 | int originalValue = 1; 54 | 55 | Result result = await IntToStringAsyncFailure(originalValue) 56 | .Bind(StringIntoIntAsyncFailure); 57 | 58 | Assert.False(result.Success); 59 | Assert.Equal(default(int), result.Value); 60 | Assert.Single(result.Errors); 61 | Assert.Contains("error", result.Errors.First().Message); 62 | } 63 | 64 | 65 | [Fact] 66 | public async Task TestBindWithNonAsyncMethodInTheMiddle() 67 | { 68 | int originalValue = 1; 69 | 70 | Result result = await IntToStringAsync(originalValue) // <- async value 71 | .Bind(StringIntoInt) // <- Sincronous method 72 | .Bind(IntToStringAsync); // <- async metohd 73 | 74 | Assert.True(result.Success); 75 | Assert.Equal(originalValue.ToString(), result.Value); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestCombine.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace ROP.UnitTest 7 | { 8 | public class TestCombine : BaseResultTest 9 | { 10 | [Fact] 11 | public void TestCombineAllCorrect() 12 | { 13 | int originalValue = 1; 14 | 15 | Result<(string, int)> result = IntToString(originalValue) 16 | .Combine(StringIntoInt); 17 | 18 | Assert.True(result.Success); 19 | Assert.Equal(originalValue.ToString(), result.Value.Item1); 20 | Assert.Equal(originalValue, result.Value.Item2); 21 | } 22 | 23 | [Fact] 24 | public async Task TestCombineAsyncAllCorrect() 25 | { 26 | int originalValue = 1; 27 | 28 | Result<(string, int)> result = await IntToStringAsync(originalValue) 29 | .Combine(StringIntoIntAsync); 30 | 31 | Assert.True(result.Success); 32 | Assert.Equal(originalValue.ToString(), result.Value.Item1); 33 | Assert.Equal(originalValue, result.Value.Item2); 34 | } 35 | 36 | 37 | [Fact] 38 | public void TestCombineWithFailure() 39 | { 40 | int originalValue = 1; 41 | 42 | Result<(string, int)> result = IntToString(originalValue) 43 | .Combine(StringIntoIntFailure); 44 | 45 | Assert.False(result.Success); 46 | Assert.Single(result.Errors); 47 | Assert.Contains("error", result.Errors.First().Message); 48 | } 49 | 50 | [Fact] 51 | public async Task TestCombineWithFailureAsync() 52 | { 53 | int originalValue = 1; 54 | 55 | Result<(string, int)> result = await IntToStringAsyncFailure(originalValue) 56 | .Combine(StringIntoIntAsyncFailure); 57 | 58 | Assert.False(result.Success); 59 | Assert.Single(result.Errors); 60 | Assert.Contains("error", result.Errors.First().Message); 61 | } 62 | 63 | [Fact] 64 | public async Task TestCombineWithFaiulreMapsToCorrectError() 65 | { 66 | int originalValue = 1; 67 | 68 | Result result = await StringIntoIntAsyncFailure(originalValue.ToString()) 69 | .Combine(IntToStringAsync) 70 | .Map(_=>true); 71 | 72 | Assert.False(result.Success); 73 | Assert.Single(result.Errors); 74 | Assert.Equal(HttpStatusCode.NotFound, result.HttpStatusCode); 75 | } 76 | 77 | [Fact] 78 | public async Task TestCombineWithFaiulreMapsToCorrectError2() 79 | { 80 | int originalValue = 1; 81 | 82 | Result result = await IntToStringAsync(originalValue) 83 | .Combine(StringIntoIntAsyncFailure) 84 | .Map(_ => true); 85 | 86 | Assert.False(result.Success); 87 | Assert.Single(result.Errors); 88 | Assert.Equal(HttpStatusCode.NotFound, result.HttpStatusCode); 89 | } 90 | 91 | [Fact] 92 | public async Task TestCombindWithNonAsyncMethodInTheMiddle() 93 | { 94 | int originalValue = 1; 95 | 96 | Result result = await IntToStringAsync(originalValue) // <- async value 97 | .Combine(StringIntoInt) // <- Sincronous method 98 | .Bind(x => x.Item1.Success().Async()); // <- async metohd 99 | 100 | Assert.True(result.Success); 101 | Assert.Equal(originalValue.ToString(), result.Value); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestFallback.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace ROP.UnitTest 5 | { 6 | public class TestFallback 7 | { 8 | 9 | [Fact] 10 | public void TestFallbackFAllsInsideFallback() 11 | { 12 | var result = 13 | MetodoOriginal(1) 14 | .Bind(x => 15 | MetodoQueFalla(x) 16 | .Fallback(_=>MetodoQueDevuelveNumeroDeMeses(x)) 17 | ); 18 | 19 | Assert.True(result.Success); 20 | Assert.Equal(12, result.Value); 21 | 22 | } 23 | 24 | 25 | [Fact] 26 | public void TestFallbackWhenMethodDoesntFail() 27 | { 28 | var result = 29 | MetodoOriginal(2) 30 | .Bind(x => 31 | MatodoQueMultiplica(x) 32 | .Fallback(_ => MetodoQueDevuelveNumeroDeMeses(x)) 33 | ); 34 | 35 | Assert.True(result.Success); 36 | Assert.Equal(4, result.Value); 37 | } 38 | 39 | [Fact] 40 | public async Task TestFallbackWithNonAsyncMethodInTheMiddle() 41 | { 42 | int originalValue = 1; 43 | 44 | Result result = await MetodoQueFalla(originalValue).Async() // <- async value 45 | .Fallback(x => MetodoOriginal(1)) // <- Sincronous method 46 | .Bind(MatodoQueMultiplica); // <- async metohd 47 | 48 | Assert.True(result.Success); 49 | Assert.Equal(originalValue, result.Value); 50 | } 51 | 52 | private Result MetodoOriginal(int i) 53 | { 54 | return i; 55 | } 56 | 57 | private Result MetodoQueFalla(int i) 58 | { 59 | return Result.Failure("error"); 60 | } 61 | 62 | private Result MatodoQueMultiplica(int i) 63 | { 64 | return i * i; 65 | } 66 | 67 | private Result MetodoQueDevuelveNumeroDeMeses(int i) 68 | { 69 | return 12; 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestIgnore.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace ROP.UnitTest 5 | { 6 | public class TestIgnore : BaseResultTest 7 | { 8 | [Fact] 9 | public void TestIgnoreGetsIgnored() 10 | { 11 | var result = 1.Success().Ignore(); 12 | 13 | Assert.IsType>(result); 14 | } 15 | 16 | [Fact] 17 | public async Task TestIgnoreGetsIgnoredAsync() 18 | { 19 | var result = await 1.Success().Async().Ignore(); 20 | 21 | Assert.IsType>(result); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestMap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace ROP.UnitTest 8 | { 9 | public class TestMap : BaseResultTest 10 | { 11 | 12 | [Fact] 13 | public void TestMapAllCorrect() 14 | { 15 | int originalValue = 1; 16 | 17 | Result result = IntToString(originalValue) 18 | .Map(MapToInt); 19 | 20 | Assert.True(result.Success); 21 | Assert.Equal(originalValue, result.Value); 22 | } 23 | 24 | [Fact] 25 | public async Task TestMapAsyncAllCorrect() 26 | { 27 | int originalValue = 1; 28 | 29 | Result result = await IntToStringAsync(originalValue) 30 | .Map(MapToIntAsync); 31 | 32 | Assert.True(result.Success); 33 | Assert.Equal(originalValue, result.Value); 34 | } 35 | 36 | [Fact] 37 | public async Task TestMapAsyncWithSyncInTheMiddle() 38 | { 39 | int originalValue = 1; 40 | 41 | Result> result = await IntToStringAsync(originalValue) 42 | .Map(a=>new List(){a}); 43 | 44 | Assert.True(result.Success); 45 | Assert.Single(result.Value); 46 | Assert.Equal("1", result.Value.First()); 47 | } 48 | 49 | 50 | private int MapToInt(string s) 51 | { 52 | return Convert.ToInt32(s); 53 | } 54 | private Task MapToIntAsync(string s) 55 | { 56 | return Task.FromResult(Convert.ToInt32(s)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestSuccessHTTPStatusCode.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | using System.Net; 4 | 5 | namespace ROP.UnitTest 6 | { 7 | public class TestSuccessHTTPStatusCode : BaseResultTest 8 | { 9 | [Fact] 10 | public async Task TestSuccessHTTPStatusCode_SetSTatusCode_thenHttpStatuscodeIsUpdated() 11 | { 12 | Result result = await 1.Success() 13 | .Async() 14 | .UseSuccessHttpStatusCode(HttpStatusCode.OK); 15 | 16 | Assert.True(result.Success); 17 | Assert.Equal(HttpStatusCode.OK, result.HttpStatusCode); 18 | } 19 | 20 | [Fact] 21 | public async Task TestSuccessHTTPStatusCode_thenDefaultHttpStatuscodeIsUpdated() 22 | { 23 | Result result = await 1.Success() 24 | .Async(); 25 | 26 | Assert.True(result.Success); 27 | Assert.Equal(HttpStatusCode.OK, result.HttpStatusCode); 28 | } 29 | 30 | //TODO: choose which version do i want to use, only the "useSuccessStatusCode" at the end of the chain or at any point. 31 | //use .UseSuccessHttpStatusCode(r.HttpStatusCode) in the chain to st the value. 32 | [Fact] 33 | public void TestSuccessHTTPStatusCode_thenDefaultHttpStatuscodeIsUpdated_thenMoreChain() 34 | { 35 | Result result = 1.Success() 36 | .UseSuccessHttpStatusCode(HttpStatusCode.OK) 37 | .Bind(IntToString); 38 | 39 | Assert.True(result.Success); 40 | Assert.Equal(HttpStatusCode.OK, result.HttpStatusCode); 41 | } 42 | 43 | 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestThen.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace ROP.UnitTest 5 | { 6 | public class TestThen : BaseResultTest 7 | { 8 | [Fact] 9 | public void TestThenGetsIgnoredForTheResult() 10 | { 11 | int originalValue = 1; 12 | 13 | Result result = 14 | originalValue.Success() 15 | .Bind(IntToString) 16 | .Then(StringIntoInt); 17 | 18 | Assert.True(result.Success); 19 | Assert.Equal(originalValue.ToString(), result.Value); 20 | } 21 | 22 | [Fact] 23 | public async Task TestThenAsyncGetsIgnoredForTheResult() 24 | { 25 | int originalValue = 1; 26 | 27 | Result result = await originalValue.Success().Async() 28 | .Bind(IntToStringAsync) 29 | .Then(StringIntoIntAsync); 30 | 31 | Assert.True(result.Success); 32 | Assert.Equal(originalValue.ToString(), result.Value); 33 | } 34 | 35 | [Fact] 36 | public async Task TestThenWithNonAsyncMethodInTheMiddleGetsIgnoredForTheResult() 37 | { 38 | int originalValue = 1; 39 | 40 | Result result = await originalValue.Success().Async() // <- async value 41 | .Then(IntToString) // <- Sincronous method 42 | .Bind(IntToStringAsync) // <- async metohd 43 | .Then(StringIntoIntAsync); 44 | 45 | Assert.True(result.Success); 46 | Assert.Equal(originalValue.ToString(), result.Value); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /test/ROP.UnitTest/TestTraverse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace ROP.UnitTest 6 | { 7 | public class TestTraverse 8 | { 9 | 10 | [Fact] 11 | public void TestTraverseConvertListResultIntoResultList() 12 | { 13 | 14 | Result> result = GetResultList() 15 | .Traverse(); 16 | 17 | Assert.True(result.Success); 18 | Assert.Equal(10, result.Value.Count); 19 | } 20 | 21 | [Fact] 22 | public async Task TestTraverseConvertListResultIntoResultListAsync() 23 | { 24 | 25 | Result> result = await GetResultListAsync() 26 | .Traverse(); 27 | 28 | Assert.True(result.Success); 29 | Assert.Equal(10, result.Value.Count); 30 | } 31 | 32 | private List> GetResultList() 33 | { 34 | List> list = new List>(); 35 | for (int i = 0; i <10; i++) 36 | { 37 | list.Add(i.Success()); 38 | } 39 | return list; 40 | } 41 | 42 | private List>> GetResultListAsync() 43 | { 44 | List>> list = new List>>(); 45 | for (int i = 0; i < 10; i++) 46 | { 47 | list.Add(i.Success().Async()); 48 | } 49 | return list; 50 | } 51 | } 52 | } 53 | --------------------------------------------------------------------------------