Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions src/Exceptionless.Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text.Json;
using AutoMapper;
using Exceptionless.Core.Authentication;
using Exceptionless.Core.Billing;
using Exceptionless.Core.Configuration;
Expand Down Expand Up @@ -197,21 +196,6 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO
services.AddSingleton<StackService>();

services.AddTransient<IDomainLoginProvider, ActiveDirectoryLoginProvider>();

services.AddTransient<Profile, CoreMappings>();
services.AddSingleton<IMapper>(s =>
{
var profiles = s.GetServices<Profile>();
var c = new MapperConfiguration(cfg =>
{
cfg.ConstructServicesUsing(s.GetRequiredService);

foreach (var profile in profiles)
cfg.AddProfile(profile);
});

return c.CreateMapper();
});
}

public static void LogConfiguration(IServiceProvider serviceProvider, AppOptions appOptions, ILogger logger)
Expand Down
1 change: 0 additions & 1 deletion src/Exceptionless.Core/Exceptionless.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
<EmbeddedResource Include="Mail\Templates\user-password-reset.html" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="4.0.1" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="Foundatio.Extensions.Hosting" Version="13.0.0-beta1.7" />
Expand Down
5 changes: 0 additions & 5 deletions src/Exceptionless.Core/Models/CoreMappings.cs

This file was deleted.

38 changes: 2 additions & 36 deletions src/Exceptionless.Web/Bootstrapper.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
using AutoMapper;
using Exceptionless.Core;
using Exceptionless.Core.Extensions;
using Exceptionless.Core.Jobs.WorkItemHandlers;
using Exceptionless.Core.Models;
using Exceptionless.Core.Models.Data;
using Exceptionless.Core.Queues.Models;
using Exceptionless.Web.Hubs;
using Exceptionless.Web.Models;
using Exceptionless.Web.Mapping;
using Foundatio.Extensions.Hosting.Startup;
using Foundatio.Jobs;
using Foundatio.Messaging;
using Token = Exceptionless.Core.Models.Token;

namespace Exceptionless.Web;

Expand All @@ -21,7 +17,7 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO
services.AddSingleton<WebSocketConnectionManager>();
services.AddSingleton<MessageBusBroker>();

services.AddTransient<Profile, ApiMappings>();
services.AddSingleton<ApiMapper>();

Core.Bootstrapper.RegisterServices(services, appOptions);
Insulation.Bootstrapper.RegisterServices(services, appOptions, appOptions.RunJobsInProcess);
Expand All @@ -46,34 +42,4 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO
services.AddSingleton<EnqueueOrganizationNotificationOnPlanOverage>();
services.AddStartupAction<EnqueueOrganizationNotificationOnPlanOverage>();
}

public class ApiMappings : Profile
{
public ApiMappings(TimeProvider timeProvider)
{
CreateMap<UserDescription, EventUserDescription>();

CreateMap<NewOrganization, Organization>();
CreateMap<Organization, ViewOrganization>().AfterMap((o, vo) =>
{
vo.IsOverMonthlyLimit = o.IsOverMonthlyLimit(timeProvider);
});

CreateMap<Stripe.Invoice, InvoiceGridModel>().AfterMap((si, igm) =>
{
igm.Id = igm.Id.Substring(3);
igm.Date = si.Created;
});

CreateMap<NewProject, Project>();
CreateMap<Project, ViewProject>().AfterMap((p, vp) => vp.HasSlackIntegration = p.Data is not null && p.Data.ContainsKey(Project.KnownDataKeys.SlackToken));

CreateMap<NewToken, Token>().ForMember(m => m.Type, m => m.Ignore());
CreateMap<Token, ViewToken>();

CreateMap<User, ViewUser>();

CreateMap<NewWebHook, WebHook>();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
using AutoMapper;
using Exceptionless.Core.Extensions;
using Exceptionless.Core.Extensions;
using Exceptionless.Core.Models;
using Exceptionless.Core.Queries.Validation;
using Exceptionless.Web.Mapping;
using Foundatio.Repositories;
using Foundatio.Repositories.Models;
using Microsoft.AspNetCore.Mvc;

namespace Exceptionless.Web.Controllers;

public abstract class ReadOnlyRepositoryApiController<TRepository, TModel, TViewModel> : ExceptionlessApiController where TRepository : ISearchableReadOnlyRepository<TModel> where TModel : class, IIdentity, new() where TViewModel : class, IIdentity, new()
public abstract class ReadOnlyRepositoryApiController<TRepository, TModel, TViewModel> : ExceptionlessApiController
where TRepository : ISearchableReadOnlyRepository<TModel>
where TModel : class, IIdentity, new()
where TViewModel : class, IIdentity, new()
{
protected readonly TRepository _repository;
protected static readonly bool _isOwnedByOrganization = typeof(IOwnedByOrganization).IsAssignableFrom(typeof(TModel));
protected static readonly bool _isOrganization = typeof(TModel) == typeof(Organization);
protected static readonly bool _supportsSoftDeletes = typeof(ISupportSoftDeletes).IsAssignableFrom(typeof(TModel));
protected static readonly IReadOnlyCollection<TModel> EmptyModels = new List<TModel>(0).AsReadOnly();
protected readonly IMapper _mapper;
protected readonly ApiMapper _mapper;
protected readonly IAppQueryValidator _validator;
protected readonly ILogger _logger;

public ReadOnlyRepositoryApiController(TRepository repository, IMapper mapper, IAppQueryValidator validator, TimeProvider timeProvider, ILoggerFactory loggerFactory) : base(timeProvider)
public ReadOnlyRepositoryApiController(TRepository repository, ApiMapper mapper, IAppQueryValidator validator, TimeProvider timeProvider, ILoggerFactory loggerFactory) : base(timeProvider)
{
_repository = repository;
_mapper = mapper;
Expand All @@ -38,9 +41,21 @@ protected async Task<ActionResult<TViewModel>> GetByIdImplAsync(string id)

protected virtual async Task<ActionResult<TViewModel>> OkModelAsync(TModel model)
{
return Ok(await MapAsync<TViewModel>(model, true));
var viewModel = MapToViewModel(model);
await AfterResultMapAsync([viewModel]);
return Ok(viewModel);
}

/// <summary>
/// Maps a domain model to a view model. Override in derived controllers.
/// </summary>
protected abstract TViewModel MapToViewModel(TModel model);

/// <summary>
/// Maps a collection of domain models to view models. Override in derived controllers.
/// </summary>
protected abstract List<TViewModel> MapToViewModels(IEnumerable<TModel> models);

protected virtual async Task<TModel?> GetModelAsync(string id, bool useCache = true)
{
if (String.IsNullOrEmpty(id))
Expand Down Expand Up @@ -69,24 +84,6 @@ protected virtual async Task<IReadOnlyCollection<TModel>> GetModelsAsync(string[
return models;
}

protected async Task<TDestination> MapAsync<TDestination>(object source, bool isResult = false)
{
var destination = _mapper.Map<TDestination>(source);
if (isResult)
await AfterResultMapAsync(new List<TDestination>(new[] { destination }));

return destination;
}

protected async Task<ICollection<TDestination>> MapCollectionAsync<TDestination>(object source, bool isResult = false)
{
var destination = _mapper.Map<ICollection<TDestination>>(source);
if (isResult)
await AfterResultMapAsync<TDestination>(destination);

return destination;
}

protected virtual Task AfterResultMapAsync<TDestination>(ICollection<TDestination> models)
{
foreach (var model in models.OfType<IData>())
Expand Down
32 changes: 24 additions & 8 deletions src/Exceptionless.Web/Controllers/Base/RepositoryApiController.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
using AutoMapper;
using Exceptionless.Core.Extensions;
using Exceptionless.Core.Extensions;
using Exceptionless.Core.Models;
using Exceptionless.Core.Queries.Validation;
using Exceptionless.Web.Extensions;
using Exceptionless.Web.Mapping;
using Exceptionless.Web.Utility;
using Foundatio.Repositories;
using Foundatio.Repositories.Models;
using Microsoft.AspNetCore.Mvc;

namespace Exceptionless.Web.Controllers;

public abstract class RepositoryApiController<TRepository, TModel, TViewModel, TNewModel, TUpdateModel> : ReadOnlyRepositoryApiController<TRepository, TModel, TViewModel> where TRepository : ISearchableRepository<TModel> where TModel : class, IIdentity, new() where TViewModel : class, IIdentity, new() where TNewModel : class, new() where TUpdateModel : class, new()
public abstract class RepositoryApiController<TRepository, TModel, TViewModel, TNewModel, TUpdateModel> : ReadOnlyRepositoryApiController<TRepository, TModel, TViewModel>
where TRepository : ISearchableRepository<TModel>
where TModel : class, IIdentity, new()
where TViewModel : class, IIdentity, new()
where TNewModel : class, new()
where TUpdateModel : class, new()
{
public RepositoryApiController(TRepository repository, IMapper mapper, IAppQueryValidator validator,
public RepositoryApiController(TRepository repository, ApiMapper mapper, IAppQueryValidator validator,
TimeProvider timeProvider, ILoggerFactory loggerFactory) : base(repository, mapper, validator, timeProvider, loggerFactory) { }

/// <summary>
/// Maps a new model (from API input) to a domain model. Override in derived controllers.
/// </summary>
protected abstract TModel MapToModel(TNewModel newModel);

protected async Task<ActionResult<TViewModel>> PostImplAsync(TNewModel value)
{
if (value is null)
return BadRequest();

var mapped = await MapAsync<TModel>(value);
var mapped = MapToModel(value);
// if no organization id is specified, default to the user's 1st associated org.
if (!_isOrganization && mapped is IOwnedByOrganization orgModel && String.IsNullOrEmpty(orgModel.OrganizationId) && GetAssociatedOrganizationIds().Count > 0)
orgModel.OrganizationId = Request.GetDefaultOrganizationId()!;
Expand All @@ -32,7 +42,9 @@ protected async Task<ActionResult<TViewModel>> PostImplAsync(TNewModel value)
var model = await AddModelAsync(mapped);
await AfterAddAsync(model);

return Created(new Uri(GetEntityLink(model.Id) ?? throw new InvalidOperationException()), await MapAsync<TViewModel>(model, true));
var viewModel = MapToViewModel(model);
await AfterResultMapAsync([viewModel]);
return Created(new Uri(GetEntityLink(model.Id) ?? throw new InvalidOperationException()), viewModel);
}

protected async Task<ActionResult<TViewModel>> UpdateModelAsync(string id, Func<TModel, Task<TModel>> modelUpdateFunc)
Expand All @@ -50,7 +62,9 @@ protected async Task<ActionResult<TViewModel>> UpdateModelAsync(string id, Func<
if (typeof(TViewModel) == typeof(TModel))
return Ok(model);

return Ok(await MapAsync<TViewModel>(model, true));
var viewModel = MapToViewModel(model);
await AfterResultMapAsync([viewModel]);
return Ok(viewModel);
}

protected async Task<ActionResult<TViewModel>> UpdateModelsAsync(string[] ids, Func<TModel, Task<TModel>> modelUpdateFunc)
Expand All @@ -70,7 +84,9 @@ protected async Task<ActionResult<TViewModel>> UpdateModelsAsync(string[] ids, F
if (typeof(TViewModel) == typeof(TModel))
return Ok(models);

return Ok(await MapAsync<TViewModel>(models, true));
var viewModels = MapToViewModels(models);
await AfterResultMapAsync(viewModels);
return Ok(viewModels);
}

protected virtual string? GetEntityLink(string id)
Expand Down
20 changes: 15 additions & 5 deletions src/Exceptionless.Web/Controllers/EventController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text;
using AutoMapper;
using Exceptionless.Core;
using Exceptionless.Core.Authorization;
using Exceptionless.Core.Extensions;
Expand All @@ -16,6 +15,7 @@
using Exceptionless.Core.Services;
using Exceptionless.DateTimeExtensions;
using Exceptionless.Web.Extensions;
using Exceptionless.Web.Mapping;
using Exceptionless.Web.Models;
using Exceptionless.Web.Utility;
using Exceptionless.Web.Utility.OpenApi;
Expand Down Expand Up @@ -60,7 +60,7 @@ public EventController(IEventRepository repository,
FormattingPluginManager formattingPluginManager,
ICacheClient cacheClient,
JsonSerializerSettings jsonSerializerSettings,
IMapper mapper,
ApiMapper mapper,
PersistentEventQueryValidator validator,
AppOptions appOptions,
TimeProvider timeProvider,
Expand All @@ -82,6 +82,11 @@ ILoggerFactory loggerFactory
DefaultDateField = EventIndex.Alias.Date;
}

// Mapping implementations - PersistentEvent uses itself as view model (no mapping needed)
protected override PersistentEvent MapToModel(PersistentEvent newModel) => newModel;
protected override PersistentEvent MapToViewModel(PersistentEvent model) => model;
protected override List<PersistentEvent> MapToViewModels(IEnumerable<PersistentEvent> models) => models.ToList();

/// <summary>
/// Count
/// </summary>
Expand Down Expand Up @@ -811,9 +816,14 @@ public async Task<IActionResult> SetUserDescriptionAsync(string referenceId, Use
// Set the project for the configuration response filter.
Request.SetProject(project);

var eventUserDescription = await MapAsync<EventUserDescription>(description);
eventUserDescription.ProjectId = project.Id;
eventUserDescription.ReferenceId = referenceId;
var eventUserDescription = new EventUserDescription
{
ProjectId = project.Id,
ReferenceId = referenceId,
EmailAddress = description.EmailAddress,
Description = description.Description,
Data = description.Data
};

await _eventUserDescriptionQueue.EnqueueAsync(eventUserDescription);
return StatusCode(StatusCodes.Status202Accepted);
Expand Down
25 changes: 17 additions & 8 deletions src/Exceptionless.Web/Controllers/OrganizationController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using AutoMapper;
using Exceptionless.Core;
using Exceptionless.Core;
using Exceptionless.Core.Authorization;
using Exceptionless.Core.Billing;
using Exceptionless.Core.Extensions;
Expand All @@ -12,6 +11,7 @@
using Exceptionless.Core.Repositories.Queries;
using Exceptionless.Core.Services;
using Exceptionless.Web.Extensions;
using Exceptionless.Web.Mapping;
using Exceptionless.Web.Models;
using Exceptionless.Web.Utility;
using Foundatio.Caching;
Expand Down Expand Up @@ -55,7 +55,7 @@ public OrganizationController(
UsageService usageService,
IMailer mailer,
IMessagePublisher messagePublisher,
IMapper mapper,
ApiMapper mapper,
IAppQueryValidator validator,
AppOptions options,
TimeProvider timeProvider,
Expand All @@ -74,6 +74,11 @@ public OrganizationController(
_options = options;
}

// Mapping implementations
protected override Organization MapToModel(NewOrganization newModel) => _mapper.MapToOrganization(newModel);
protected override ViewOrganization MapToViewModel(Organization model) => _mapper.MapToViewOrganization(model);
protected override List<ViewOrganization> MapToViewModels(IEnumerable<Organization> models) => _mapper.MapToViewOrganizations(models);

/// <summary>
/// Get all
/// </summary>
Expand All @@ -82,10 +87,11 @@ public OrganizationController(
public async Task<ActionResult<IReadOnlyCollection<ViewOrganization>>> GetAllAsync(string? mode = null)
{
var organizations = await GetModelsAsync(GetAssociatedOrganizationIds().ToArray());
var viewOrganizations = await MapCollectionAsync<ViewOrganization>(organizations, true);
var viewOrganizations = MapToViewModels(organizations);
await AfterResultMapAsync(viewOrganizations);

if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase))
return Ok(await PopulateOrganizationStatsAsync(viewOrganizations.ToList()));
return Ok(await PopulateOrganizationStatsAsync(viewOrganizations));

return Ok(viewOrganizations);
}
Expand All @@ -98,7 +104,8 @@ public async Task<ActionResult<IReadOnlyCollection<ViewOrganization>>> GetForAdm
page = GetPage(page);
limit = GetLimit(limit);
var organizations = await _repository.GetByCriteriaAsync(criteria, o => o.PageNumber(page).PageLimit(limit), sort, paid, suspended);
var viewOrganizations = (await MapCollectionAsync<ViewOrganization>(organizations.Documents, true)).ToList();
var viewOrganizations = MapToViewModels(organizations.Documents);
await AfterResultMapAsync(viewOrganizations);

if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase))
return OkWithResourceLinks(await PopulateOrganizationStatsAsync(viewOrganizations), organizations.HasMore, page, organizations.Total);
Expand Down Expand Up @@ -127,7 +134,9 @@ public async Task<ActionResult<ViewOrganization>> GetAsync(string id, string? mo
if (organization is null)
return NotFound();

var viewOrganization = await MapAsync<ViewOrganization>(organization, true);
var viewOrganization = MapToViewModel(organization);
await AfterResultMapAsync<ViewOrganization>([viewOrganization]);

if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase))
return Ok(await PopulateOrganizationStatsAsync(viewOrganization));

Expand Down Expand Up @@ -306,7 +315,7 @@ public async Task<ActionResult<IReadOnlyCollection<InvoiceGridModel>>> GetInvoic
var client = new StripeClient(_options.StripeOptions.StripeApiKey);
var invoiceService = new InvoiceService(client);
var invoiceOptions = new InvoiceListOptions { Customer = organization.StripeCustomerId, Limit = limit + 1, EndingBefore = before, StartingAfter = after };
var invoices = (await MapCollectionAsync<InvoiceGridModel>(await invoiceService.ListAsync(invoiceOptions), true)).ToList();
var invoices = _mapper.MapToInvoiceGridModels(await invoiceService.ListAsync(invoiceOptions));
return OkWithResourceLinks(invoices.Take(limit).ToList(), invoices.Count > limit);
}

Expand Down
Loading
Loading