A functionally written ASP.NET Core codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the RealWorld spec and API.
This codebase was created to demonstrate a fully fledged backend application built with ASP.NET Core and 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 repo.
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:
public Task<Option<UserModel, Error>> LoginAsync(CredentialsModel model) =>
GetUser(u => u.Email == model.Email)
.FilterAsync<User, Error>(user => UserManager.CheckPasswordAsync(user, model.Password), "Invalid credentials.")
.MapAsync(async user =>
{
var result = Mapper.Map<UserModel>(user);
result.Token = GenerateToken(user.Id, user.Email);
return result;
});
You can read more about Maybe and Either here and here.
This application has been made using the Dev Adventures .NET Core template, therefore it follows the architecture of and has all of the features that the template provides.
/// <summary>
/// Retreives a user's profile by username.
/// </summary>
/// <param name="username">The username to look for.</param>
/// <returns>A user profile or not found.</returns>
/// <response code="200">Returns the user's profile.</response>
/// <response code="404">No user with tha given username was found.</response>
[HttpGet("{username}")]
[ProducesResponseType(typeof(UserProfileModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Error), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Get(string username) =>
(await _profilesService.ViewProfileAsync(CurrentUserId.SomeNotNull(), username))
.Match<IActionResult>(profile => Ok(new { profile }), Error);
public interface IProfilesService
{
Task<Option<UserProfileModel, Error>> FollowAsync(string followerId, string userToFollowUsername);
Task<Option<UserProfileModel, Error>> UnfollowAsync(string followerId, string userToUnfollowUsername);
Task<Option<UserProfileModel, Error>> ViewProfileAsync(Option<string> viewingUserId, string profileUsername);
}
public Task<Option<UserProfileModel, Error>> 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);
}));
public class GetArticlesModel
{
public Option<string> Tag { get; set; }
public Option<string> Author { get; set; }
public Option<string> 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
{
"messages": [
"The Email field is not a valid email.",
"The LastName field is required.",
"The FirstName field is required."
]
}
// 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."
]
}
[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));
}
src/server/Conduit.Api/appsettings.json
to a running Postgres instance. Set the database name to an unexisting one.dotnet restore
dotnet build
dotnet ef database update
dotnet run