Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/dnikolovv/dev-adventures-realworld
A RealWorld application implemented using the Dev Adventures .NET Core template and functional programming.
https://github.com/dnikolovv/dev-adventures-realworld
Last synced: about 2 months ago
JSON representation
A RealWorld application implemented using the Dev Adventures .NET Core template and functional programming.
- Host: GitHub
- URL: https://github.com/dnikolovv/dev-adventures-realworld
- Owner: dnikolovv
- Created: 2018-09-21T15:39:29.000Z (about 6 years ago)
- Default Branch: master
- Last Pushed: 2022-12-08T01:28:34.000Z (about 2 years ago)
- Last Synced: 2024-08-02T14:07:55.468Z (5 months ago)
- Language: C#
- Homepage: https://marketplace.visualstudio.com/items?itemName=dnikolovv.dev-adventures-project-setup
- Size: 271 KB
- Stars: 142
- Watchers: 8
- Forks: 29
- Open Issues: 10
-
Metadata Files:
- Readme: readme.md
Awesome Lists containing this project
- awesome-starred-test - dnikolovv/dev-adventures-realworld - A RealWorld application implemented using the Dev Adventures .NET Core template and functional programming. (C# #)
README
[![Build Status](https://travis-ci.com/dnikolovv/dev-adventures-realworld.svg?branch=master)](https://travis-ci.com/dnikolovv/dev-adventures-realworld)
# ![RealWorld Example App](logo.png)
> ### A functionally written ASP.NET Core codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API.
### [RealWorld Repo](https://github.com/gothinkster/realworld)
This codebase was created to demonstrate a fully fledged backend application built with **ASP.NET Core** and **[Optional](https://github.com/nlkl/Optional)**. It includes CRUD operations, authentication, routing, pagination, and more.
It completely adheres to the **ASP.NET Core** community styleguides & best practices.
For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.
# Features
What's special about this specific implementation is that it employs a different approach on error handling and propagation. It uses the **Maybe** and **Either** monads to enable very explicit function declarations and allow us to abstract the conditionals and validations into the type itself.
This allows you to do cool stuff like:
```csharp
public Task> LoginAsync(CredentialsModel model) =>
GetUser(u => u.Email == model.Email)
.FilterAsync(user => UserManager.CheckPasswordAsync(user, model.Password), "Invalid credentials.")
.MapAsync(async user =>
{
var result = Mapper.Map(user);result.Token = GenerateToken(user.Id, user.Email);
return result;
});
```You can read more about **Maybe** and **Either** [here](https://devadventures.net/2018/04/17/forget-object-reference-not-set-to-an-instance-of-an-object-functional-adventures-in-c/) and [here](https://devadventures.net/2018/09/20/real-life-examples-of-functional-c-sharp-either/).
# Architecture
This application has been made using the [Dev Adventures .NET Core template](https://marketplace.visualstudio.com/items?itemName=dnikolovv.dev-adventures-project-setup), therefore it follows the architecture of and has all of the features that the template provides.
- [x] Swagger UI + Fully Documented Controllers
![swagger-ui](./img/swagger-ui.JPG)
- [x] Thin Controllers
```csharp
///
/// Retreives a user's profile by username.
///
/// The username to look for.
/// A user profile or not found.
/// Returns the user's profile.
/// No user with tha given username was found.
[HttpGet("{username}")]
[ProducesResponseType(typeof(UserProfileModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Error), StatusCodes.Status400BadRequest)]
public async Task Get(string username) =>
(await _profilesService.ViewProfileAsync(CurrentUserId.SomeNotNull(), username))
.Match(profile => Ok(new { profile }), Error);
```- [x] Robust service layer using the [Either](https://devadventures.net/2018/09/20/real-life-examples-of-functional-c-sharp-either/) monad.
```csharp
public interface IProfilesService
{
Task> FollowAsync(string followerId, string userToFollowUsername);Task> UnfollowAsync(string followerId, string userToUnfollowUsername);
Task> ViewProfileAsync(Option viewingUserId, string profileUsername);
}
``````csharp
public Task> FollowAsync(string followerId, string userToFollowUsername) =>
GetUserByIdOrError(followerId).FlatMapAsync(user =>
GetUserByNameOrError(userToFollowUsername)
.FilterAsync(async u => u.Id != followerId, "A user cannot follow himself.")
.FilterAsync(async u => user.Following.All(fu => fu.FollowingId != u.Id), "You are already following this user")
.FlatMapAsync(async userToFollow =>
{
DbContext.FollowedUsers.Add(new FollowedUser
{
FollowerId = followerId,
FollowingId = userToFollow.Id
});await DbContext.SaveChangesAsync();
return await ViewProfileAsync(followerId.Some(), userToFollow.UserName);
}));
```- [x] Safe query string parameter model binding using the [Option](https://github.com/nlkl/Optional) monad.
```csharp
public class GetArticlesModel
{
public Option Tag { get; set; }public Option Author { get; set; }
public Option Favorited { get; set; }
public int Limit { get; set; } = 20;
public int Offset { get; set; } = 0;
}
```- [x] AutoMapper
- [x] EntityFramework Core with ~~SQL Server~~ Postgres and ASP.NET Identity
- [x] JWT authentication/authorization
- [x] File logging with Serilog
- [x] Stylecop
- [x] Neat folder structure
```
├───src
│ ├───configuration
│ └───server
│ ├───Conduit.Api
│ ├───Conduit.Business
│ ├───Conduit.Core
│ ├───Conduit.Data
│ └───Conduit.Data.EntityFramework
└───tests
└───Conduit.Business.Tests
```- [x] Global Model Errors Handler
```json
{
"messages": [
"The Email field is not a valid email.",
"The LastName field is required.",
"The FirstName field is required."
]
}
```- [x] Global Environment-Dependent Exception Handler
```json
// Development
{
"ClassName": "System.Exception",
"Message": null,
"Data": null,
"InnerException": null,
"HelpURL": null,
"StackTraceString": "...",
"RemoteStackTraceString": null,
"RemoteStackIndex": 0,
"ExceptionMethod": null,
"HResult": -2146233088,
"Source": "Conduit.Api",
"WatsonBuckets": null
}// Production
{
"messages": [
"An unexpected internal server error has occurred."
]
}
```- [x] Neatly organized solution structure
![solution-structure](./img/solution-structure.JPG)### Test Suite
- [x] xUnit
- [x] Autofixture
- [x] Moq
- [x] Shouldly
- [x] Arrange Act Assert Pattern```csharp
[Theory]
[AutoData]
public async Task Login_Should_Return_Exception_When_Credentials_Are_Invalid(CredentialsModel model, User expectedUser)
{
// Arrange
AddUserWithEmail(model.Email, expectedUser);MockCheckPassword(model.Password, false);
// Act
var result = await _usersService.LoginAsync(model);// Assert
result.HasValue.ShouldBeFalse();
result.MatchNone(error => error.Messages?.Count.ShouldBeGreaterThan(0));
}
```# Getting started
1. Set the connection string in `src/server/Conduit.Api/appsettings.json` to a running Postgres instance. Set the database name to an unexisting one.
2. Execute `dotnet restore`
3. Execute `dotnet build`
4. Execute `dotnet ef database update`
5. Execute `dotnet run`