This repo contains a library created by Georgia EPD-IT to provide common classes and tools for our web applications.
(Much of this work was inspired by the ABP Framework.)
To install, search for "GaEpd.AppLibrary" in the NuGet package manager or run the following command:
dotnet add package GaEpd.AppLibrary
The following interfaces and abstract implementations of domain entities are provided for domain-driven design:
- The basic
IEntity<TKey>interface defines an entity with a primary key of the given type. - The special case
IEntityinterface defines an entity with aGUIDprimary key. IAuditableEntity<TUserKey>adds created/updated properties and methods for basic data auditing.ISoftDeleteandISoftDelete<TUserKey>add properties for "soft deleting" an entity rather than actually- deleting it.
INamedEntityadds a "Name" string property.IActiveEntityadds an "Active" boolean property.
There are also abstract classes based on the above interfaces from which you should derive your domain
entities: Entity<TKey>, AuditableEntity<TKey, TUserKey>, SoftDeleteEntity<TKey, TUserKey>,
AuditableSoftDeleteEntity<TKey, TUserKey>, and StandardNamedEntity.
The StandardNamedEntity class derives from AuditableEntity<Guid>, INamedEntity, and IActiveEntity, and includes
methods for enforcing the length of the Name. Maximum and minimum length for the name can be set by overriding the
default properties.
Example usage:
public class MyEntity : StandardNamedEntity
{
public override int MinNameLength => 2;
public override int MaxNameLength => 50;
public MyEntity() { }
public MyEntity(Guid id, string name) : base(id, name) { }
}An abstract ValueObject record can help add value objects to your domain entities.
A value object is a compound of properties, such as an address or
date range, that are comparable based solely on their values rather than their references. The properties of a value
object are typically stored with its parent class, not as a separate record with its own ID. Value objects should be
treated as immutable. When deriving from ValueObject, you must override the GetEqualityComponents() method to
define which properties to use to determine equality.
Example usage:
[Owned]
public record Address : ValueObject
{
public string Street { get; init; } = string.Empty;
public string? Street2 { get; init; }
public string City { get; init; } = string.Empty;
public string State { get; init; } = string.Empty;
[DataType(DataType.PostalCode)]
public string PostalCode { get; init; } = string.Empty;
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return Street2 ?? string.Empty;
yield return City;
yield return State;
yield return PostalCode;
}
}Note: The [Owned] attribute is an Entity Framework attribute defining this as a value object owned by the parent
class. See Owned Entity Types for more info on how
this is implemented in EF Core.
Common data repository interfaces define basic entity CRUD operations.
- The
IReadRepositoryinterface defines various read-only operations: Get, Find, Count, Exists, GetList, and GetPagedList. (The Get and Find methods both return an entity, but Get enables the Entity Framework change tracker while Find disables it. Also, if an entity is not found, then Get throws anEntityNotFoundExceptionwhile Find returns null. Read more about Tracking vs. No-Tracking Queries.) - The
IWriteRepositoryinterface defines various write operations: Insert, Update, and Delete. IRepositorycombines the above read and write interfaces.INamedEntityRepositoryderives fromIRepositoryand adds some methods useful for entities expected to have unique names (i.e., entities implementingINamedEntity).IReadRepositoryWithMappingadds overloads to the Find, GetList, and GetPagedList methods that use query projection to return data transfer objects (DTOs) rather than complete entities. Use of these methods enables Entity Framework to create much more efficient SQL queries.IRepositoryWithMappingandINamedEntityRepositoryWithMappingsimilarly include the query projection overloads.
There are two sets of abstract repository classes that implement the IRepository, IRepositoryWithMapping,
INamedEntityRepository, and INamedEntityRepositoryWithMapping interfaces. Client applications can derive from either
or both of these implementations.
- One set (in the
LocalRepositorynamespace) uses in-memory data (convenient for use during development when the overhead of a database is undesirable). - The other set (in the
EFRepositorynamespace) requires an Entity Framework database context (DbContext).
Example usage:
// Define the repository interface.
public interface IMyRepository : INamedEntityRepository<MyEntity> { }
// Implement a version of the repository that uses in-memory data.
public sealed class MyInMemoryRepository
: LocalRepository.NamedEntityRepository<MyEntity>(MyEntitySeedData), IMyRepository;
// Implement another version that uses an Entity Framework database.
public sealed class MyEfRepository(AppDbContext context)
: EFRepository.NamedEntityRepository<MyEntity, AppDbContext>(context), IMyRepository;
// Using dependency injection, choose which implementation to use based on the app environment, for example.
if (builder.Environment.IsDevelopment())
{
builder.Services.AddSingleton<IMyRepository, MyInMemoryRepository>();
}
else
{
builder.Services.AddScoped<IMyRepository, MyEfRepository>();
}Code from C# in a Nutshell is included to enable creating
filter expressions that can be combined. The library comes with the commonly used filters WithId(id) for all entities
and ExcludedDeleted() for "soft delete" entities.
Example usage:
public static Expression<Func<MyEntity, bool>> IsActive(this Expression<Func<MyEntity, bool>> predicate) =>
predicate.And(e => e.IsActive);
public static Expression<Func<MyEntity, bool>> ActiveAndNotDeletedPredicate() =>
PredicateBuilder.True<MyEntity>().IsActive().ExcludeDeleted();IPaginatedRequest and IPaginatedResult<T> define how to request and receive paginated (and sorted) search results.
The System.Linq.Dynamic.Core package is included.
A ListItem<TKey> record type defines a key-value pair with fields for ID of type TKey and string Name.
The ToSelectList() extension method takes a ListItem enumerable and returns an MVC SelectList which can be used to
create an HTML <select> element.
GetDisplayName() and GetDescription() return the DisplayAttribute.Name and DescriptionAttribute values of an
enum, respectively.
The GuardClauses package is included by reference.
